Skip to content

convertToEs6Module: Avoid replacing entire function #22507

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

Merged
3 commits merged into from
Apr 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
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
53 changes: 35 additions & 18 deletions src/services/codefixes/convertToEs6Module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ namespace ts.codefix {
return false;
}
case SyntaxKind.BinaryExpression: {
const { left, operatorToken, right } = expression as BinaryExpression;
return operatorToken.kind === SyntaxKind.EqualsToken && convertAssignment(sourceFile, checker, statement as ExpressionStatement, left, right, changes, exports);
const { operatorToken } = expression as BinaryExpression;
return operatorToken.kind === SyntaxKind.EqualsToken && convertAssignment(sourceFile, checker, expression as BinaryExpression, changes, exports);
}
}
}
Expand Down Expand Up @@ -144,7 +144,7 @@ namespace ts.codefix {
return convertPropertyAccessImport(name, initializer.name.text, initializer.expression.arguments[0], identifiers);
}
else {
// Move it out to its own variable statement.
// Move it out to its own variable statement. (This will not be used if `!foundImport`)
return createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([decl], declarationList.flags));
}
});
Expand Down Expand Up @@ -177,33 +177,32 @@ namespace ts.codefix {
function convertAssignment(
sourceFile: SourceFile,
checker: TypeChecker,
statement: ExpressionStatement,
left: Expression,
right: Expression,
assignment: BinaryExpression,
changes: textChanges.ChangeTracker,
exports: ExportRenames,
): ModuleExportsChanged {
const { left, right } = assignment;
if (!isPropertyAccessExpression(left)) {
return false;
}

if (isExportsOrModuleExportsOrAlias(sourceFile, left)) {
if (isExportsOrModuleExportsOrAlias(sourceFile, right)) {
// `const alias = module.exports;` or `module.exports = alias;` can be removed.
changes.deleteNode(sourceFile, statement);
changes.deleteNode(sourceFile, assignment.parent);
}
else {
let newNodes = isObjectLiteralExpression(right) ? tryChangeModuleExportsObject(right) : undefined;
let changedToDefaultExport = false;
if (!newNodes) {
([newNodes, changedToDefaultExport] = convertModuleExportsToExportDefault(right, checker));
}
changes.replaceNodeWithNodes(sourceFile, statement, newNodes);
changes.replaceNodeWithNodes(sourceFile, assignment.parent, newNodes);
return changedToDefaultExport;
}
}
else if (isExportsOrModuleExportsOrAlias(sourceFile, left.expression)) {
convertNamedExport(sourceFile, statement, left.name, right, changes, exports);
convertNamedExport(sourceFile, assignment as BinaryExpression & { left: PropertyAccessExpression }, changes, exports);
}

return false;
Expand All @@ -223,7 +222,7 @@ namespace ts.codefix {
case SyntaxKind.SpreadAssignment:
return undefined;
case SyntaxKind.PropertyAssignment:
return !isIdentifier(prop.name) ? undefined : convertExportsDotXEquals(prop.name.text, prop.initializer);
return !isIdentifier(prop.name) ? undefined : convertExportsDotXEquals_replaceNode(prop.name.text, prop.initializer);
case SyntaxKind.MethodDeclaration:
return !isIdentifier(prop.name) ? undefined : functionExpressionToDeclaration(prop.name.text, [createToken(SyntaxKind.ExportKeyword)], prop);
default:
Expand All @@ -234,28 +233,26 @@ namespace ts.codefix {

function convertNamedExport(
sourceFile: SourceFile,
statement: Statement,
propertyName: Identifier,
right: Expression,
assignment: BinaryExpression & { left: PropertyAccessExpression },
changes: textChanges.ChangeTracker,
exports: ExportRenames,
): void {
// If "originalKeywordKind" was set, this is e.g. `exports.
const { text } = propertyName;
const { text } = assignment.left.name;
const rename = exports.get(text);
if (rename !== undefined) {
/*
const _class = 0;
export { _class as class };
*/
const newNodes = [
makeConst(/*modifiers*/ undefined, rename, right),
makeConst(/*modifiers*/ undefined, rename, assignment.right),
makeExportDeclaration([createExportSpecifier(rename, text)]),
];
changes.replaceNodeWithNodes(sourceFile, statement, newNodes);
changes.replaceNodeWithNodes(sourceFile, assignment.parent, newNodes);
}
else {
changes.replaceNode(sourceFile, statement, convertExportsDotXEquals(text, right));
convertExportsPropertyAssignment(assignment, sourceFile, changes);
}
}

Expand Down Expand Up @@ -303,7 +300,27 @@ namespace ts.codefix {
return makeExportDeclaration([createExportSpecifier(/*propertyName*/ undefined, "default")], moduleSpecifier);
}

function convertExportsDotXEquals(name: string | undefined, exported: Expression): Statement {
function convertExportsPropertyAssignment({ left, right, parent }: BinaryExpression & { left: PropertyAccessExpression }, sourceFile: SourceFile, changes: textChanges.ChangeTracker): void {
const name = left.name.text;
if ((isFunctionExpression(right) || isArrowFunction(right) || isClassExpression(right)) && (!right.name || right.name.text === name)) {
// `exports.f = function() {}` -> `export function f() {}` -- Replace `exports.f = ` with `export `, and insert the name after `function`.
changes.replaceRange(sourceFile, { pos: left.getStart(sourceFile), end: right.getStart(sourceFile) }, createToken(SyntaxKind.ExportKeyword), { suffix: " " });

if (!right.name) changes.insertName(sourceFile, right, name);

const semi = findChildOfKind(parent, SyntaxKind.SemicolonToken, sourceFile);
if (semi) changes.deleteNode(sourceFile, semi, { useNonAdjustedEndPosition: true });
Copy link
Member

Choose a reason for hiding this comment

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

Will this clobber trailing trivia on the semicolon?

Copy link
Author

Choose a reason for hiding this comment

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

}
else {
// `exports.f = function g() {}` -> `export const f = function g() {}` -- just replace `exports.` with `export const `
changes.replaceNodeRangeWithNodes(sourceFile, left.expression, findChildOfKind(left, SyntaxKind.DotToken, sourceFile)!,
[createToken(SyntaxKind.ExportKeyword), createToken(SyntaxKind.ConstKeyword)],
{ joiner: " ", suffix: " " });
}
}

// TODO: GH#22492 this will cause an error if a change has been made inside the body of the node.
function convertExportsDotXEquals_replaceNode(name: string | undefined, exported: Expression): Statement {
const modifiers = [createToken(SyntaxKind.ExportKeyword)];
switch (exported.kind) {
case SyntaxKind.FunctionExpression: {
Expand Down
43 changes: 38 additions & 5 deletions src/services/textChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ namespace ts.textChanges {
preserveLeadingWhitespace?: boolean;
}

export interface ReplaceWithMultipleNodesOptions extends InsertNodeOptions {
readonly joiner?: string;
}

enum ChangeKind {
Remove,
ReplaceWithSingleNode,
Expand Down Expand Up @@ -130,7 +134,7 @@ namespace ts.textChanges {
interface ReplaceWithMultipleNodes extends BaseChange {
readonly kind: ChangeKind.ReplaceWithMultipleNodes;
readonly nodes: ReadonlyArray<Node>;
readonly options?: InsertNodeOptions;
readonly options?: ReplaceWithMultipleNodesOptions;
}

interface ChangeText extends BaseChange {
Expand Down Expand Up @@ -303,7 +307,7 @@ namespace ts.textChanges {
this.replaceRange(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNode, options);
}

private replaceRangeWithNodes(sourceFile: SourceFile, range: TextRange, newNodes: ReadonlyArray<Node>, options: InsertNodeOptions = {}) {
private replaceRangeWithNodes(sourceFile: SourceFile, range: TextRange, newNodes: ReadonlyArray<Node>, options: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd = {}) {
this.changes.push({ kind: ChangeKind.ReplaceWithMultipleNodes, sourceFile, range, options, nodes: newNodes });
return this;
}
Expand All @@ -312,15 +316,15 @@ namespace ts.textChanges {
return this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, options), newNodes, options);
}

public replaceNodeRangeWithNodes(sourceFile: SourceFile, startNode: Node, endNode: Node, newNodes: ReadonlyArray<Node>, options: ChangeNodeOptions = useNonAdjustedPositions) {
public replaceNodeRangeWithNodes(sourceFile: SourceFile, startNode: Node, endNode: Node, newNodes: ReadonlyArray<Node>, options: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd = useNonAdjustedPositions) {
return this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNodes, options);
}

private insertNodeAt(sourceFile: SourceFile, pos: number, newNode: Node, options: InsertNodeOptions = {}) {
this.replaceRange(sourceFile, createTextRange(pos), newNode, options);
}

private insertNodesAt(sourceFile: SourceFile, pos: number, newNodes: ReadonlyArray<Node>, options: InsertNodeOptions = {}): void {
private insertNodesAt(sourceFile: SourceFile, pos: number, newNodes: ReadonlyArray<Node>, options: ReplaceWithMultipleNodesOptions = {}): void {
this.changes.push({ kind: ChangeKind.ReplaceWithMultipleNodes, sourceFile, options, nodes: newNodes, range: { pos, end: pos } });
}

Expand Down Expand Up @@ -477,6 +481,35 @@ namespace ts.textChanges {
return Debug.failBadSyntaxKind(node); // We haven't handled this kind of node yet -- add it
}

public insertName(sourceFile: SourceFile, node: FunctionExpression | ClassExpression | ArrowFunction, name: string): void {
Debug.assert(!node.name);
if (node.kind === SyntaxKind.ArrowFunction) {
const arrow = findChildOfKind(node, SyntaxKind.EqualsGreaterThanToken, sourceFile)!;
const lparen = findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile);
if (lparen) {
// `() => {}` --> `function f() {}`
Copy link
Member

Choose a reason for hiding this comment

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

Does this work for () => true?

Copy link
Member

Choose a reason for hiding this comment

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

(Or is there some reason that's impossible here?)

Copy link
Author

Choose a reason for hiding this comment

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

See if (node.body.kind !== SyntaxKind.Block) below

this.insertNodesAt(sourceFile, lparen.getStart(sourceFile), [createToken(SyntaxKind.FunctionKeyword), createIdentifier(name)], { joiner: " " });
this.deleteNode(sourceFile, arrow);
Copy link
Member

Choose a reason for hiding this comment

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

Will this destroy any trivia? Does it matter?

Copy link
Author

Choose a reason for hiding this comment

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

This is a bug already, see #23373

}
else {
// `x => {}` -> `function f(x) {}`
this.insertText(sourceFile, first(node.parameters).getStart(sourceFile), `function ${name}(`);
// Replacing full range of arrow to get rid of the leading space -- replace ` =>` with `)`
this.replaceRange(sourceFile, arrow, createToken(SyntaxKind.CloseParenToken));
}

if (node.body.kind !== SyntaxKind.Block) {
// `() => 0` => `function f() { return 0; }`
this.insertNodesAt(sourceFile, node.body.getStart(sourceFile), [createToken(SyntaxKind.OpenBraceToken), createToken(SyntaxKind.ReturnKeyword)], { joiner: " ", suffix: " " });
this.insertNodesAt(sourceFile, node.body.end, [createToken(SyntaxKind.SemicolonToken), createToken(SyntaxKind.CloseBraceToken)], { joiner: " " });
}
}
else {
const pos = findChildOfKind(node, node.kind === SyntaxKind.FunctionExpression ? SyntaxKind.FunctionKeyword : SyntaxKind.ClassKeyword, sourceFile)!.end;
this.insertNodeAt(sourceFile, pos, createIdentifier(name), { prefix: " " });
}
}

/**
* This function should be used to insert nodes in lists when nodes don't carry separators as the part of the node range,
* i.e. arguments in arguments lists, parameters in parameter lists etc.
Expand Down Expand Up @@ -646,7 +679,7 @@ namespace ts.textChanges {
const { options = {}, range: { pos } } = change;
const format = (n: Node) => getFormattedTextOfNode(n, sourceFile, pos, options, newLineCharacter, formatContext, validate);
const text = change.kind === ChangeKind.ReplaceWithMultipleNodes
? change.nodes.map(n => removeSuffix(format(n), newLineCharacter)).join(newLineCharacter)
? change.nodes.map(n => removeSuffix(format(n), newLineCharacter)).join(change.options.joiner || newLineCharacter)
: format(change.node);
// strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line
const noIndent = (options.preserveLeadingWhitespace || options.indentation !== undefined || getLineStartPositionForPosition(pos, sourceFile) === pos) ? text : text.replace(/^\s+/, "");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
verify.codeFix({
description: "Convert to ES6 module",
newFileContent: `
export function f() { }
export function f() {}
`,
});
15 changes: 11 additions & 4 deletions tests/cases/fourslash/refactorConvertToEs6Module_export_named.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
////[|exports.f = function() {}|];
////exports.C = class {};
////exports.x = 0;
////exports.a1 = () => {};
////exports.a2 = () => 0;
////exports.a3 = x => { x; };
////exports.a4 = x => x;

verify.getSuggestionDiagnostics([{
message: "File is a CommonJS module; it may be converted to an ES6 module.",
Expand All @@ -15,8 +19,11 @@ verify.getSuggestionDiagnostics([{
verify.codeFix({
description: "Convert to ES6 module",
newFileContent:
`export function f() { }
export class C {
}
export const x = 0;`,
`export function f() {}
export class C {}
export const x = 0;
export function a1() {}
export function a2() { return 0; }
export function a3(x) { x; }
export function a4(x) { return x; }`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />

// @allowJs: true

// @Filename: /a.js
////exports.C = class E { static instance = new E(); }
////exports.D = class D { static instance = new D(); }

verify.codeFix({
description: "Convert to ES6 module",
newFileContent:
`export const C = class E { static instance = new E(); }
export class D { static instance = new D(); }`,
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
verify.codeFix({
description: "Convert to ES6 module",
newFileContent:
`export const f = function g() { g(); };
`export const f = function g() { g(); }
export function h() { h(); }`,
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
////
////exports.z = 2;
////exports.f = function(z) {
//// z;
//// exports.z; z;
////}

// TODO: GH#22492 Should be a able access `exports.z` inside `exports.f`
Expand All @@ -28,8 +28,9 @@ const _y = y;
export { _y as y };
_y;

export const z = 2;
const _z = 2;
export { _z as z };
export function f(z) {
z;
_z; z;
}`,
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,5 @@ verify.codeFix({
description: "Convert to ES6 module",
newFileContent:
`export async function* f(p) { p; }
export class C extends D {
m() { }
}`,
export class C extends D { m() {} }`,
});