Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
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");
`,
},
});