Skip to content

Commit

Permalink
Added support for refactorings that share an AST traversal run.
Browse files Browse the repository at this point in the history
  • Loading branch information
visusnet committed Feb 21, 2020
1 parent cda8f0d commit 06a19de
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 51 deletions.
81 changes: 75 additions & 6 deletions src/action-providers.ts
@@ -1,9 +1,9 @@
import * as vscode from "vscode";

import { createSelectionFromVSCode } from "./editor/adapters/vscode-editor";
import { Selection } from "./editor/selection";
import { RefactoringWithActionProvider } from "./types";
import { RefactoringWithActionProvider, isLegacyActionProvider } from "./types";
import * as t from "./ast";
import { Selection } from "./editor/selection";

export { RefactoringActionProvider };

Expand All @@ -21,18 +21,87 @@ class RefactoringActionProvider implements vscode.CodeActionProvider {
const ast = t.parse(document.getText());
const selection = createSelectionFromVSCode(range);

return this.refactorings
.filter(refactoring => this.canPerform(refactoring, ast, selection))
.map(refactoring => this.buildCodeActionFor(refactoring));
const applicableRefactorings: RefactoringWithActionProvider[] = [];

const onCanPeform = (
path: t.NodePath<any>,
refactoring: RefactoringWithActionProvider
) => {
if (isLegacyActionProvider(refactoring.actionProvider)) {
return;
}

if (refactoring.actionProvider.updateMessage) {
refactoring.actionProvider.updateMessage(path);
}

applicableRefactorings.push(refactoring);
};

t.traverseAST(ast, {
enter: (path: t.NodePath<any>) => {
this.refactorings.forEach(refactoring =>
this.canPerform(refactoring, path, selection, onCanPeform)
);
}
});

const applicableLegacyRefactorings = this.refactorings.filter(refactoring =>
this.canPerformLegacy(refactoring, ast, selection)
);

return [...applicableRefactorings, ...applicableLegacyRefactorings].map(
refactoring => this.buildCodeActionFor(refactoring)
);
}

private canPerform(
refactoring: RefactoringWithActionProvider,
path: t.NodePath<any>,
selection: Selection,
onCanPerform: (
matchedPath: t.NodePath<any>,
refactoring: RefactoringWithActionProvider
) => void
) {
if (isLegacyActionProvider(refactoring.actionProvider)) {
return;
}

const visitor: t.Visitor = refactoring.actionProvider.createVisitor(
selection,
path => onCanPerform(path, refactoring),
refactoring
);

this.visit(visitor, path);
}

private visit(visitor: any, path: t.NodePath<any>) {
const node: t.Node = path.node;

try {
if (typeof visitor[node.type] === "function") {
visitor[node.type](path);
} else if (typeof visitor[node.type] === "object") {
visitor[node.type].enter(path);
}
} catch (_) {
// Silently fail, we don't care why it failed (e.g. code can't be parsed).
}
}

private canPerformLegacy(
refactoring: RefactoringWithActionProvider,
ast: t.AST,
selection: Selection
) {
try {
return refactoring.actionProvider.canPerform(ast, selection);
return (
isLegacyActionProvider(refactoring.actionProvider) &&
typeof refactoring.actionProvider.canPerform === "function" &&
refactoring.actionProvider.canPerform(ast, selection)
);
} catch (_) {
// Silently fail, we don't care why it failed (e.g. code can't be parsed).
return false;
Expand Down
Expand Up @@ -211,19 +211,6 @@ console.log({
});`,
expectedPosition: new Position(2, 7)
},
{
description: "an object property value which key is not in camel case",
code: `console.log({
hello_world: "World",
goodbye: "my old friend"
});`,
selection: Selection.cursorAt(1, 16),
expected: `const hello_world = "World";
console.log({
hello_world,
goodbye: "my old friend"
});`
},
{
description:
"an element nested in a multi-lines object that is assigned to a variable",
Expand Down Expand Up @@ -358,14 +345,6 @@ console.log({
selection: Selection.cursorAt(0, 17),
expected: `const { node } = path;
console.log(node.name);`
},
{
description:
"a member expression when property name is not in camel case",
code: `console.log(path.some_node.name);`,
selection: Selection.cursorAt(0, 17),
expected: `const { some_node } = path;
console.log(some_node.name);`
},
{
description: "member expression with computed value",
Expand Down
13 changes: 7 additions & 6 deletions src/refactorings/extract-variable/extract-variable.ts
Expand Up @@ -341,7 +341,7 @@ class Variable {
const { node } = path;

if (ast.isStringLiteral(node)) {
this.tryToSetNameWith(camel(node.value));
this.tryToSetNameWith(node.value);
}

if (ast.canBeShorthand(path)) {
Expand Down Expand Up @@ -374,7 +374,8 @@ class Variable {
}

private tryToSetNameWith(value: string) {
const startsWithNumber = value.match(/^\d.*/);
const parsedName = camel(value);
const startsWithNumber = parsedName.match(/^\d.*/);

const BLACKLISTED_KEYWORDS = [
"const",
Expand All @@ -391,12 +392,12 @@ class Variable {
];

if (
value.length > 1 &&
value.length <= 20 &&
parsedName.length > 1 &&
parsedName.length <= 20 &&
!startsWithNumber &&
!BLACKLISTED_KEYWORDS.includes(value)
!BLACKLISTED_KEYWORDS.includes(parsedName)
) {
this._name = value;
this._name = parsedName;
}
}
}
Expand Down
13 changes: 11 additions & 2 deletions src/refactorings/simplify-ternary/index.ts
@@ -1,4 +1,8 @@
import { simplifyTernary, hasTernaryToSimplify } from "./simplify-ternary";
import {
simplifyTernary,
createTernaryToSimplifyVisitor
} from "./simplify-ternary";
import * as t from "../../ast";

import { RefactoringWithActionProvider } from "../../types";

Expand All @@ -10,7 +14,12 @@ const config: RefactoringWithActionProvider = {
},
actionProvider: {
message: "Simplify ternary",
canPerform: hasTernaryToSimplify
createVisitor: createTernaryToSimplifyVisitor,
updateMessage(path: t.NodePath<any>): void {
this.message = `Simplify ternary (for demo purposes only: ${
path.node.type
})`;
}
}
};

Expand Down
10 changes: 1 addition & 9 deletions src/refactorings/simplify-ternary/simplify-ternary.ts
Expand Up @@ -2,7 +2,7 @@ import { Editor, Code, ErrorReason } from "../../editor/editor";
import { Selection } from "../../editor/selection";
import * as t from "../../ast";

export { simplifyTernary, hasTernaryToSimplify };
export { simplifyTernary, createVisitor as createTernaryToSimplifyVisitor };

async function simplifyTernary(
code: Code,
Expand All @@ -19,14 +19,6 @@ async function simplifyTernary(
await editor.write(updatedCode.code);
}

function hasTernaryToSimplify(ast: t.AST, selection: Selection): boolean {
let result = false;

t.traverseAST(ast, createVisitor(selection, () => (result = true)));

return result;
}

function updateCode(ast: t.AST, selection: Selection): t.Transformed {
return t.transformAST(
ast,
Expand Down
38 changes: 31 additions & 7 deletions src/types.ts
@@ -1,8 +1,13 @@
import { Code, Editor } from "./editor/editor";
import { Selection } from "./editor/selection";
import { AST } from "./ast";
import { AST, Visitor, NodePath } from "./ast";

export { Refactoring, RefactoringWithActionProvider, Operation };
export {
Refactoring,
RefactoringWithActionProvider,
Operation,
isLegacyActionProvider
};

interface Refactoring {
command: {
Expand All @@ -11,21 +16,40 @@ interface Refactoring {
};
}

interface ActionProvider {
message: string;
isPreferred?: boolean;
createVisitor: (
selection: Selection,
onMatch: (path: NodePath<any>) => void,
refactoring: RefactoringWithActionProvider
) => Visitor;
updateMessage?: (path: NodePath<any>) => void;
}

interface LegacyActionProvider {
message: string;
isPreferred?: boolean;
canPerform: (ast: AST, selection: Selection) => boolean;
}

interface RefactoringWithActionProvider extends Refactoring {
command: {
key: string;
title: string;
operation: Operation;
};
actionProvider: {
message: string;
canPerform: (ast: AST, selection: Selection) => boolean;
isPreferred?: boolean;
};
actionProvider: ActionProvider | LegacyActionProvider;
}

type Operation = (
code: Code,
selection: Selection,
write: Editor
) => Promise<void>;

function isLegacyActionProvider(
actionProvider: ActionProvider | LegacyActionProvider
): actionProvider is LegacyActionProvider {
return (actionProvider as LegacyActionProvider).canPerform !== undefined;
}

0 comments on commit 06a19de

Please sign in to comment.