Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Inline Variable on destructured object patterns #34

Merged
merged 29 commits into from
Sep 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ef86423
Handle basic Object Pattern
Aug 30, 2019
cd7aa00
Remove duplicated concept
nicoespeon Sep 13, 2019
215235e
Handle renamed destructured variable
nicoespeon Sep 13, 2019
e72289c
Handle assignments to things that are not objects
nicoespeon Sep 14, 2019
a0bd804
Compute init name with different use cases
nicoespeon Sep 14, 2019
864ad1f
Merge guard clauses
nicoespeon Sep 14, 2019
9ea4fd7
Extract getInlinableCodeFromDeclaration()
nicoespeon Sep 14, 2019
7e8f9f2
Extract child creation from composite
nicoespeon Sep 14, 2019
49cbacf
Handle nested object pattern
nicoespeon Sep 14, 2019
506a17d
Move isShadowIn() in AST
nicoespeon Sep 14, 2019
0edb20c
Extract findInlinableCode()
nicoespeon Sep 14, 2019
fc69cdf
Introduce base class CompositeInlinable
nicoespeon Sep 14, 2019
cb629de
Test InlinableObjectPattern updates
nicoespeon Sep 14, 2019
77046e9
Handle complex member expressions
nicoespeon Sep 14, 2019
7c1565c
Extract destructured object pattern tests
nicoespeon Sep 14, 2019
dede27f
Extract default selection
nicoespeon Sep 14, 2019
7c66cf0
Handle multi-line object pattern
nicoespeon Sep 14, 2019
dd6392a
Handle object pattern in a multiple declaration
nicoespeon Sep 15, 2019
4f8b21b
Pass selection to findInlinableCode()
nicoespeon Sep 15, 2019
8500496
Check all object pattern properties
nicoespeon Sep 15, 2019
578a051
Introduce notion of SingleDeclaration()
nicoespeon Sep 17, 2019
9653b07
Pass declarationSelection as a parameter
nicoespeon Sep 17, 2019
87f41f7
Change implementation of declaration selection again
nicoespeon Sep 17, 2019
183e086
Handle cases with many other elements destructured
nicoespeon Sep 17, 2019
f18f684
Test we don't inline on invalid selections
nicoespeon Sep 17, 2019
5a4e566
Enlarge definition of SelectableObjectProperty
nicoespeon Sep 18, 2019
c631a5b
Handle rest elements
nicoespeon Sep 18, 2019
7a72173
Update Changelog
nicoespeon Sep 18, 2019
730e89b
Fix check to be type-safe
nicoespeon Sep 18, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

#### Inline Variable now handles destructured object patterns

Consider the following code:

```js
const { userId } = session;
messages.map(message => ({ userId }));
```

If you tried to inline `userId`, it wouldn't work because destructured object patterns were not supported.

Now it would work as expected:

```js
messages.map(message => ({ userId: session.userId }));
```

Thanks to @noway for [bringing this one up](https://github.com/nicoespeon/abracadabra/issues/25).

Destructured array patterns (e.g. `const [userId] = session`) are still not supported, but we're working on it.

## [0.7.0] - 2019-09-16

### Added
Expand Down
38 changes: 37 additions & 1 deletion src/ast/scope.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { NodePath } from "@babel/traverse";
import * as t from "@babel/types";

export { findScopePath, findParentIfPath, getFunctionScopePath };
import { areEqual } from "./domain";

export { findScopePath, findParentIfPath, getFunctionScopePath, isShadowIn };

function findScopePath(path: NodePath<t.Node | null>): NodePath | undefined {
return path.findParent(
Expand Down Expand Up @@ -30,3 +32,37 @@ function findParentIfPath(
function getFunctionScopePath(path: NodePath<t.FunctionDeclaration>): NodePath {
return path.getFunctionParent() || path.parentPath;
}

function isShadowIn(
id: t.Identifier,
ancestors: t.TraversalAncestors
): boolean {
// A variable is "shadow" if one of its ancestor redefines the Identifier.
return ancestors.some(
({ node }) => isDeclaredInFunction(node) || isDeclaredInScope(node)
);

function isDeclaredInFunction(node: t.Node): boolean {
return (
t.isFunctionDeclaration(node) &&
node.params.some(node => areEqual(id, node))
);
}

function isDeclaredInScope(node: t.Node): boolean {
return (
t.isBlockStatement(node) &&
node.body.some(
child =>
t.isVariableDeclaration(child) &&
child.declarations.some(
declaration =>
t.isVariableDeclarator(declaration) &&
areEqual(id, declaration.id) &&
// Of course, if it's the inlined variable it's not a shadow!
declaration.id !== id
)
)
);
}
}
21 changes: 19 additions & 2 deletions src/ast/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export {
Selectable,
isSelectableNode,
isSelectableVariableDeclarator,
isSelectableIdentifier
isSelectableIdentifier,
isSelectableObjectProperty
};

interface ASTSelection {
Expand All @@ -27,11 +28,16 @@ interface ASTPosition {

type SelectablePath = NodePath<SelectableNode>;
type SelectableNode = Selectable<t.Node>;
type SelectableObjectProperty = Selectable<t.ObjectProperty>;
type SelectableIdentifier = Selectable<t.Identifier>;
type SelectableVariableDeclarator = Selectable<t.VariableDeclarator>;
type Selectable<T> = T & { loc: t.SourceLocation };

interface SelectableObjectProperty extends t.ObjectProperty {
loc: t.SourceLocation;
key: Selectable<t.ObjectProperty["key"]>;
value: Selectable<t.ObjectProperty["value"]>;
}

function isSelectableNode(node: t.Node | null): node is SelectableNode {
return !!node && !!node.loc;
}
Expand All @@ -47,3 +53,14 @@ function isSelectableVariableDeclarator(
): declaration is SelectableVariableDeclarator {
return !!declaration.loc;
}

function isSelectableObjectProperty(
property: t.Node | null
): property is SelectableObjectProperty {
return (
t.isObjectProperty(property) &&
isSelectableNode(property) &&
isSelectableNode(property.value) &&
isSelectableNode(property.key)
);
}
8 changes: 4 additions & 4 deletions src/editor/selection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe("Selection", () => {
it("should return selection that ends at given selection start", () => {
const selection = new Selection([0, 10], [3, 10]);

const extendedSelection = selection.extendEndTo(
const extendedSelection = selection.extendEndToStartOf(
new Selection([3, 12], [3, 15])
);

Expand All @@ -43,7 +43,7 @@ describe("Selection", () => {
it("should not change selection if the given one starts before our selection ends", () => {
const selection = new Selection([0, 10], [3, 10]);

const extendedSelection = selection.extendEndTo(
const extendedSelection = selection.extendEndToStartOf(
new Selection([2, 12], [3, 15])
);

Expand All @@ -55,7 +55,7 @@ describe("Selection", () => {
it("should return selection that starts at given selection end", () => {
const selection = new Selection([0, 10], [3, 10]);

const extendedSelection = selection.extendStartTo(
const extendedSelection = selection.extendStartToEndOf(
new Selection([0, 0], [0, 3])
);

Expand All @@ -65,7 +65,7 @@ describe("Selection", () => {
it("should not change selection if the given one ends after our selection starts", () => {
const selection = new Selection([0, 10], [3, 10]);

const extendedSelection = selection.extendStartTo(
const extendedSelection = selection.extendStartToEndOf(
new Selection([0, 0], [1, 3])
);

Expand Down
16 changes: 14 additions & 2 deletions src/editor/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,30 @@ class Selection {
);
}

extendStartTo(selection: Selection): Selection {
extendStartToEndOf(selection: Selection): Selection {
return selection.end.isBefore(this.start)
? Selection.fromPositions(selection.end, this.end)
: this;
}

extendEndTo(selection: Selection): Selection {
extendStartToStartOf(selection: Selection): Selection {
return selection.start.isBefore(this.start)
? Selection.fromPositions(selection.start, this.end)
: this;
}

extendEndToStartOf(selection: Selection): Selection {
return selection.start.isAfter(this.end)
? Selection.fromPositions(this.start, selection.start)
: this;
}

extendEndToEndOf(selection: Selection): Selection {
return selection.end.isAfter(this.end)
? Selection.fromPositions(this.start, selection.end)
: this;
}

isInsidePath(path: ast.NodePath): path is ast.SelectablePath {
return this.isInsideNode(path.node);
}
Expand Down
2 changes: 1 addition & 1 deletion src/refactorings/extract-variable/extract-variable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ function getOccurrenceLoc(
node: ast.SelectableNode,
selection: Selection
): ast.SourceLocation | null {
return ast.isObjectProperty(node)
return ast.isSelectableObjectProperty(node)
? findObjectPropertyLoc(selection, node)
: ast.isJSXExpressionContainer(node)
? node.expression.loc
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as ast from "../../ast";

import { Selection } from "../../editor/selection";
import { Update } from "../../editor/editor";

import { InlinableCode, InlinableObjectPattern } from "./find-inlinable-code";

describe("InlinableObjectPattern", () => {
it("should return child code updates", () => {
const updates = [
{
code: "userId",
selection: ANY_SELECTION
},
{
code: "player.state",
selection: ANY_SELECTION
}
];
const inlinable = createInlinableObjectPattern(
new FakeInlinable(updates),
""
);

const result = inlinable.updateIdentifiersWith("");

expect(result).toEqual(updates);
});

it("should prepend inlined code with init name before forwarding it to child", () => {
const child = new FakeInlinable();
jest.spyOn(child, "updateIdentifiersWith");
const inlinable = createInlinableObjectPattern(child, "user");

inlinable.updateIdentifiersWith("name");

expect(child.updateIdentifiersWith).toBeCalledWith("user.name");
});

it("should resolve inlined code path if inlined code is already a member expression", () => {
const child = new FakeInlinable();
jest.spyOn(child, "updateIdentifiersWith");
const inlinable = createInlinableObjectPattern(child, "names");

inlinable.updateIdentifiersWith("user.first");

expect(child.updateIdentifiersWith).toBeCalledWith("user.names.first");
});

it("should resolve inlined code path if inlined code is an object property", () => {
const child = new FakeInlinable();
jest.spyOn(child, "updateIdentifiersWith");
const inlinable = createInlinableObjectPattern(child, "user");

inlinable.updateIdentifiersWith("n: name");

expect(child.updateIdentifiersWith).toBeCalledWith("user.n");
});

it("should resolve inlined code path if inlined code is a complex member expression", () => {
const child = new FakeInlinable();
jest.spyOn(child, "updateIdentifiersWith");
const inlinable = createInlinableObjectPattern(child, "user");

inlinable.updateIdentifiersWith("session.data[0].first");

expect(child.updateIdentifiersWith).toBeCalledWith(
"session.data[0].user.first"
);
});
});

const ANY_SELECTION = Selection.cursorAt(0, 0);

class FakeInlinable implements InlinableCode {
isRedeclared = false;
isExported = false;
hasIdentifiersToUpdate = false;
shouldExtendSelectionToDeclaration = false;
valueSelection = ANY_SELECTION;
codeToRemoveSelection = ANY_SELECTION;

private updates: Update[];

constructor(updates: Update[] = []) {
this.updates = updates;
}

updateIdentifiersWith = () => this.updates;
}

function createInlinableObjectPattern(child: InlinableCode, initName: string) {
const ANY_LOC: ast.SourceLocation = {
start: { line: 0, column: 0 },
end: { line: 0, column: 0 }
};
const ANY_PROPERTY = ast.objectProperty(
ast.identifier(""),
ast.identifier("")
);
const ANY_SELECTABLE_PROPERTY: ast.SelectableObjectProperty = {
...ANY_PROPERTY,
loc: ANY_LOC,
key: {
...ANY_PROPERTY.key,
loc: ANY_LOC
},
value: {
...ANY_PROPERTY.value,
loc: ANY_LOC
}
};

return new InlinableObjectPattern(
child,
initName,
ANY_SELECTABLE_PROPERTY,
false
);
}
Loading