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

Add 'move to new file' refactor #23726

Merged
15 commits merged into from
May 10, 2018
2 changes: 1 addition & 1 deletion src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -4175,7 +4175,7 @@
"category": "Message",
"code": 95046
},
"Move to new file": {
"Move to a new file": {
"category": "Message",
"code": 95047
}
Expand Down
12 changes: 9 additions & 3 deletions src/compiler/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1504,6 +1504,13 @@ namespace ts {
return block;
}

/* @internal */
export function createExpressionStatement(expression: Expression): ExpressionStatement {
const node = <ExpressionStatement>createSynthesizedNode(SyntaxKind.ExpressionStatement);
node.expression = expression;
return node;
}

export function updateBlock(node: Block, statements: ReadonlyArray<Statement>) {
return node.statements !== statements
? updateNode(createBlock(statements, node.multiLine), node)
Expand All @@ -1530,9 +1537,8 @@ namespace ts {
}

export function createStatement(expression: Expression) {
const node = <ExpressionStatement>createSynthesizedNode(SyntaxKind.ExpressionStatement);
node.expression = parenthesizeExpressionForExpressionStatement(expression);
return node;
return createExpressionStatement(parenthesizeExpressionForExpressionStatement(expression));

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: extra newline.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could consider linting for no-padding.

}

export function updateStatement(node: ExpressionStatement, expression: Expression) {
Expand Down
4 changes: 2 additions & 2 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3059,10 +3059,10 @@ Actual: ${stringify(fullActual)}`);
public moveToNewFile(options: FourSlashInterface.MoveToNewFileOptions): void {
assert(this.getRanges().length === 1);
const range = this.getRanges()[0];
const refactor = ts.find(this.getApplicableRefactors(range), r => r.name === "Move to new file");
const refactor = ts.find(this.getApplicableRefactors(range), r => r.name === "Move to a new file");
assert(refactor.actions.length === 1);
const action = ts.first(refactor.actions);
assert(action.name === "Move to new file" && action.description === "Move to new file");
assert(action.name === "Move to a new file" && action.description === "Move to a new file");

const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactor.name, action.name, ts.defaultPreferences);
for (const edit of editInfo.edits) {
Expand Down
16 changes: 4 additions & 12 deletions src/services/findAllReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,27 +612,19 @@ namespace ts.FindAllReferences.Core {
checker.getPropertySymbolOfDestructuringAssignment(<Identifier>location);
}

function getObjectBindingElementWithoutPropertyName(symbol: Symbol): BindingElement | undefined {
function getObjectBindingElementWithoutPropertyName(symbol: Symbol): BindingElement & { name: Identifier } | undefined {
const bindingElement = getDeclarationOfKind<BindingElement>(symbol, SyntaxKind.BindingElement);
if (bindingElement &&
bindingElement.parent.kind === SyntaxKind.ObjectBindingPattern &&
isIdentifier(bindingElement.name) &&
!bindingElement.propertyName) {
return bindingElement;
return bindingElement as BindingElement & { name: Identifier };
}
}

function getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol: Symbol, checker: TypeChecker): Symbol | undefined {
const bindingElement = getObjectBindingElementWithoutPropertyName(symbol);
if (!bindingElement) return undefined;

const typeOfPattern = checker.getTypeAtLocation(bindingElement.parent);
const propSymbol = typeOfPattern && checker.getPropertyOfType(typeOfPattern, (<Identifier>bindingElement.name).text);
if (propSymbol && propSymbol.flags & SymbolFlags.Accessor) {
// See GH#16922
Debug.assert(!!(propSymbol.flags & SymbolFlags.Transient));
return (propSymbol as TransientSymbol).target;
}
return propSymbol;
return bindingElement && getPropertySymbolFromBindingElement(checker, bindingElement);
}

/**
Expand Down
353 changes: 295 additions & 58 deletions src/services/refactors/moveToNewFile.ts

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions src/services/textChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,16 +458,25 @@ namespace ts.textChanges {
this.insertNodeAt(sourceFile, cls.members.pos, newElement, { indentation, prefix, suffix });
}

public insertNodeAfter(sourceFile: SourceFile, after: Node, newNode: Node): this {
public insertNodeAfter(sourceFile: SourceFile, after: Node, newNode: Node): void {
const endPosition = this.insertNodeAfterWorker(sourceFile, after, newNode);
this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(after));
}

public insertNodesAfter(sourceFile: SourceFile, after: Node, newNodes: ReadonlyArray<Node>): void {
const endPosition = this.insertNodeAfterWorker(sourceFile, after, first(newNodes));
this.insertNodesAt(sourceFile, endPosition, newNodes, this.getInsertNodeAfterOptions(after));
}

private insertNodeAfterWorker(sourceFile: SourceFile, after: Node, newNode: Node): number {
if (needSemicolonBetween(after, newNode)) {
// check if previous statement ends with semicolon
// if not - insert semicolon to preserve the code from changing the meaning due to ASI
if (sourceFile.text.charCodeAt(after.end - 1) !== CharacterCodes.semicolon) {
this.replaceRange(sourceFile, createTextRange(after.end), createToken(SyntaxKind.SemicolonToken));
}
}
const endPosition = getAdjustedEndPosition(sourceFile, after, {});
return this.replaceRange(sourceFile, createTextRange(endPosition), newNode, this.getInsertNodeAfterOptions(after));
return getAdjustedEndPosition(sourceFile, after, {});
}

private getInsertNodeAfterOptions(node: Node): InsertNodeOptions {
Expand Down
11 changes: 11 additions & 0 deletions src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,17 @@ namespace ts {
return name && name.kind === SyntaxKind.Identifier ? name.escapedText : undefined;
});
}

export function getPropertySymbolFromBindingElement(checker: TypeChecker, bindingElement: BindingElement & { name: Identifier }) {
const typeOfPattern = checker.getTypeAtLocation(bindingElement.parent);
const propSymbol = typeOfPattern && checker.getPropertyOfType(typeOfPattern, bindingElement.name.text);
if (propSymbol && propSymbol.flags & SymbolFlags.Accessor) {
// See GH#16922
Debug.assert(!!(propSymbol.flags & SymbolFlags.Transient));
return (propSymbol as TransientSymbol).target;
}
return propSymbol;
}
}

// Display-part writer helpers
Expand Down
4 changes: 2 additions & 2 deletions tests/cases/fourslash/moveToNewFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
////import { a, b } from "./other";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[I'm sure this is clear from the product code, but I haven't read it in detail.] What happens if you extract an import statement. Is that disallowed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about a return statement? Can a return statement be moved to another file?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if you extract an import statement

I think we should allow this, though there's a bug if the moved import is still needed in the old file. #23968

Can a return statement be moved to another file?

Only top-level statements can be moved, and 'return' at top-level is an errorany way.

////const p = 0;
////[|const y = p + b;|]
////y;
////a; y;

verify.moveToNewFile({
newFileContents: {
Expand All @@ -13,7 +13,7 @@ verify.moveToNewFile({

import { a } from "./other";
export const p = 0;
y;`,
a; y;`,

"/y.ts":
`import { b } from "./other";
Expand Down
18 changes: 18 additions & 0 deletions tests/cases/fourslash/moveToNewFile_importEquals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// <reference path='fourslash.ts' />

// @Filename: /a.ts
////import i = require("./i");
////import j = require("./j");
////[|const y = i;|]
////j;

verify.moveToNewFile({
newFileContents: {
"/a.ts":
`import j = require("./j");
j;`,
"/y.ts":
`import i = require("./i");
const y = i;`,
},
});
39 changes: 39 additions & 0 deletions tests/cases/fourslash/moveToNewFile_js.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/// <reference path='fourslash.ts' />

// @allowJs: true

// @Filename: /a.js
////const { a, b } = require("./other");
////const p = 0;
////
////[|const y = p + b;
////const z = 0;
////exports.z = 0;|]
////a; y; z;

// @Filename: /user.ts
////const { x, y } = require("./a");

// TODO: GH#23793 This will crash if the blank line between `const p` and `const y` is removed

verify.moveToNewFile({
newFileContents: {
"/a.js":
// TODO: GH#22330
`const { y, z } = require("./y");

const { a, } = require("./other");
const p = 0;
exports.p = p;

a; y; z;`,

"/y.js":
`const { b } = require("./other");
const { p } = require("./a");
const y = p + b;
exports.y = y;
const z = 0;
exports.z = 0;`,
},
});
4 changes: 2 additions & 2 deletions tests/cases/fourslash/moveToNewFile_newModuleNameUnique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
// @Filename: /x.ts
////

// @Filename: /x0.ts
// @Filename: /x.1.ts
////

verify.moveToNewFile({
newFileContents: {
"/a.ts":
``,

"/x00.ts":
"/x.2.ts":
`export const x = 0;`,
},
});
2 changes: 1 addition & 1 deletion tests/cases/fourslash/moveToNewFile_rangeInvalid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@

for (const range of test.ranges()) {
goTo.selectRange(range);
verify.not.refactorAvailable("Move to new file")
verify.not.refactorAvailable("Move to a new file")
}
30 changes: 30 additions & 0 deletions tests/cases/fourslash/moveToNewFile_updateUses_js.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/// <reference path='fourslash.ts' />

// @allowJs: true

// @Filename: /a.js
////exports.x = 0;
////[|exports.y = 0;|]

// @Filename: /user.js
////const { x, y } = require("./a");
////

// TODO: GH#23728 Shouldn't need `////` above

verify.moveToNewFile({
newFileContents: {
"/a.js":
`exports.x = 0;
`,

"/y.js":
`exports.y = 0;`,

"/user.js":
// TODO: GH#22330
`const { x, } = require("./a");
const { y } = require("./y");
`,
},
});