diff --git a/CHANGELOG.md b/CHANGELOG.md index 61876882d..c3477025d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +#### Inline Variable now handles destructured array patterns + +Consider the following code: + +```js +const [firstName] = names; +console.log(firstName); +``` + +If you tried to inline `firstName`, it wouldn't work because destructured array patterns were not supported. + +Now it would work as expected: + +```js +console.log(names[0]); +``` + +That means Inline Variable now handles all kind of destructured variables. Making it much more flexible and handy! + ### Fixed - All refactorings Quick Fixes used to appear on Windows because of EOL. Not anymore! diff --git a/docs/demo/inline-variable-destructured-array-pattern.gif b/docs/demo/inline-variable-destructured-array-pattern.gif new file mode 100644 index 000000000..868a82a63 Binary files /dev/null and b/docs/demo/inline-variable-destructured-array-pattern.gif differ diff --git a/docs/demo/inline-variable-destructured-patterns.gif b/docs/demo/inline-variable-destructured-patterns.gif new file mode 100644 index 000000000..056c3eff7 Binary files /dev/null and b/docs/demo/inline-variable-destructured-patterns.gif differ diff --git a/src/refactorings/inline-variable-or-function/find-inlinable-code.ts b/src/refactorings/inline-variable-or-function/find-inlinable-code.ts index 989127b06..e77dd1bd4 100644 --- a/src/refactorings/inline-variable-or-function/find-inlinable-code.ts +++ b/src/refactorings/inline-variable-or-function/find-inlinable-code.ts @@ -16,7 +16,7 @@ export { function findInlinableCode( selection: Selection, parent: ast.Node, - declaration: { id: ast.Node; init: ast.Node | null } + declaration: Declaration ): InlinableCode | null { const { id, init } = declaration; if (!ast.isSelectableNode(init)) return null; @@ -57,12 +57,30 @@ function findInlinableCode( ); }); - const isTopLevelObjectPattern = ast.isVariableDeclarator(declaration); - if (result && isTopLevelObjectPattern) { - result = new InlinableTopLevelObjectPattern(result, id.loc); - } + return wrapInTopLevelPattern(result, declaration, id.loc); + } - return result; + if (ast.isArrayPattern(id)) { + if (!ast.isSelectableNode(id)) return null; + + let result: InlinableCode | null = null; + id.elements.forEach((element, index) => { + if (!selection.isInsideNode(element)) return; + if (!ast.isSelectableNode(element)) return; + + const child = findInlinableCode(selection, parent, { + id: element, + init + }); + if (!child) return; + + const previous = id.elements[index - 1]; + const next = id.elements[index + 1]; + + result = new InlinableArrayPattern(child, index, element, previous, next); + }); + + return wrapInTopLevelPattern(result, declaration, id.loc); } return null; @@ -95,6 +113,22 @@ function getInitName(init: ast.Node): string | null { return null; } +function wrapInTopLevelPattern( + child: InlinableCode | null, + declaration: Declaration, + loc: ast.SourceLocation +): InlinableCode | null { + if (!child) return child; + + const isTopLevelObjectPattern = ast.isVariableDeclarator(declaration); + + return isTopLevelObjectPattern + ? new InlinableTopLevelPattern(child, loc) + : child; +} + +type Declaration = { id: ast.Node; init: ast.Node | null }; + // 🎭 Component interface interface InlinableCode { @@ -381,7 +415,58 @@ class InlinableObjectPattern extends CompositeInlinable { } } -class InlinableTopLevelObjectPattern extends CompositeInlinable { +class InlinableArrayPattern extends CompositeInlinable { + private index: number; + private element: ast.SelectableNode; + private previous: ast.SelectableNode | undefined; + private next: ast.SelectableNode | undefined; + + constructor( + child: InlinableCode, + index: number, + element: ast.SelectableNode, + previous?: ast.Node | null, + next?: ast.Node | null + ) { + super(child); + this.index = index; + this.element = element; + + if (previous && ast.isSelectableNode(previous)) { + this.previous = previous; + } + + if (next && ast.isSelectableNode(next)) { + this.next = next; + } + } + + get shouldExtendSelectionToDeclaration(): boolean { + if (!super.shouldExtendSelectionToDeclaration) return false; + + return !this.next && !this.previous; + } + + get codeToRemoveSelection(): Selection { + if (!super.shouldExtendSelectionToDeclaration) { + return super.codeToRemoveSelection; + } + + const selection = Selection.fromAST(this.element.loc); + + if (this.previous && !this.next) { + return selection.extendStartToEndOf(Selection.fromAST(this.previous.loc)); + } + + return selection; + } + + updateIdentifiersWith(inlinedCode: Code): Update[] { + return super.updateIdentifiersWith(`${inlinedCode}[${this.index}]`); + } +} + +class InlinableTopLevelPattern extends CompositeInlinable { private loc: ast.SourceLocation; constructor(child: InlinableCode, loc: ast.SourceLocation) { diff --git a/src/refactorings/inline-variable-or-function/inline-variable.array-pattern.test.ts b/src/refactorings/inline-variable-or-function/inline-variable.array-pattern.test.ts new file mode 100644 index 000000000..902e1c110 --- /dev/null +++ b/src/refactorings/inline-variable-or-function/inline-variable.array-pattern.test.ts @@ -0,0 +1,175 @@ +import { Code } from "../../editor/editor"; +import { Selection } from "../../editor/selection"; +import { InMemoryEditor } from "../../editor/adapters/in-memory-editor"; +import { testEach } from "../../tests-helpers"; + +import { inlineVariable } from "./inline-variable"; + +describe("Inline Variable - Array Pattern", () => { + testEach<{ code: Code; selection?: Selection; expected: Code }>( + "should inline the destructured variable value", + [ + { + description: "basic scenario", + code: `const [ userId ] = session; +console.log(userId);`, + expected: `console.log(session[0]);` + }, + { + description: "init being a member expression", + code: `const [ id ] = session.user; +console.log(id);`, + expected: `console.log(session.user[0]);` + }, + { + description: "init being a member expression with a numeric literal", + code: `const [ id ] = session.users[0]; +console.log(id);`, + expected: `console.log(session.users[0][0]);` + }, + { + description: "init being a member expression with a string literal", + code: `const [ id ] = session.users["first"]; +console.log(id);`, + expected: `console.log(session.users["first"][0]);` + }, + { + description: "nested", + code: `const [ [ id ] ] = session; +console.log(id);`, + selection: Selection.cursorAt(0, 11), + expected: `console.log(session[0][0]);` + }, + { + description: "multi-line", + code: `const [ + [ + name + ] +] = session; +console.log(name);`, + selection: Selection.cursorAt(2, 4), + expected: `console.log(session[0][0]);` + }, + { + description: "with other elements destructured, before", + code: `const [ userId, firstName ] = session; +console.log(userId, firstName);`, + selection: Selection.cursorAt(0, 17), + expected: `const [ userId ] = session; +console.log(userId, session[1]);` + }, + { + description: "with other elements destructured, after", + code: `const [ userId, firstName ] = session; +console.log(userId, firstName);`, + selection: Selection.cursorAt(0, 8), + expected: `const [ , firstName ] = session; +console.log(session[0], firstName);` + }, + { + description: "with other elements destructured, before & after", + code: `const [ userId, firstName, lastName ] = session; +console.log(userId, firstName);`, + selection: Selection.cursorAt(0, 17), + expected: `const [ userId, , lastName ] = session; +console.log(userId, session[1]);` + }, + { + description: "with other elements destructured, multi-lines", + code: `const [ + userId, + [ + firstName, + lastName + ], + age +] = session; +console.log(userId, firstName);`, + selection: Selection.cursorAt(3, 4), + expected: `const [ + userId, + [ + , + lastName + ], + age +] = session; +console.log(userId, session[1][0]);` + }, + { + description: "in a multiple declaration", + code: `const name = "John", [ userId ] = session, age = 12; +console.log(userId);`, + selection: Selection.cursorAt(0, 24), + expected: `const name = "John", age = 12; +console.log(session[0]);` + }, + { + description: "with rest element", + code: `const [ user, ...others ] = session; +console.log(user);`, + expected: `const [ , ...others ] = session; +console.log(session[0]);` + }, + { + description: "with rest element and nesting", + code: `const [ [ [ name ] ], ...others ] = session; +console.log(name);`, + selection: Selection.cursorAt(0, 12), + expected: `const [ , ...others ] = session; +console.log(session[0][0][0]);` + }, + { + description: "with rest element not being a direct sibling", + code: `const [ [ [ name ] ], player, ...others ] = session; +console.log(name);`, + selection: Selection.cursorAt(0, 12), + expected: `const [ , player, ...others ] = session; +console.log(session[0][0][0]);` + }, + { + description: "with rest elements at different levels", + code: `const [ [ [ name ], ...userData ], player, ...others ] = session; +console.log(name);`, + selection: Selection.cursorAt(0, 12), + expected: `const [ [ , ...userData ], player, ...others ] = session; +console.log(session[0][0][0]);` + } + ], + async ({ code, selection = Selection.cursorAt(0, 9), expected }) => { + const result = await doInlineVariable(code, selection); + expect(result).toBe(expected); + } + ); + + testEach<{ code: Code; selection?: Selection }>( + "should should not inline the destructured variable value", + [ + { + description: "selected value is not referenced", + code: `const [ userId ] = session; +messages.map(message => ({ name }));` + }, + { + description: "many destructured elements selected", + code: `const [ userId, name ] = session; +console.log(userId);`, + selection: new Selection([0, 13], [0, 15]) + } + ], + async ({ code, selection = Selection.cursorAt(0, 9) }) => { + const result = await doInlineVariable(code, selection); + expect(result).toBe(code); + } + ); + + async function doInlineVariable( + code: Code, + selection: Selection + ): Promise { + const editor = new InMemoryEditor(code); + await inlineVariable(code, selection, editor); + return editor.code; + } +});