-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
} | ||
} | ||
} | ||
|
@@ -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)); | ||
} | ||
}); | ||
|
@@ -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; | ||
|
@@ -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: | ||
|
@@ -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); | ||
} | ||
} | ||
|
||
|
@@ -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 }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this clobber trailing trivia on the semicolon? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -100,6 +100,10 @@ namespace ts.textChanges { | |
preserveLeadingWhitespace?: boolean; | ||
} | ||
|
||
export interface ReplaceWithMultipleNodesOptions extends InsertNodeOptions { | ||
readonly joiner?: string; | ||
} | ||
|
||
enum ChangeKind { | ||
Remove, | ||
ReplaceWithSingleNode, | ||
|
@@ -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 { | ||
|
@@ -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; | ||
} | ||
|
@@ -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 } }); | ||
} | ||
|
||
|
@@ -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() {}` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this work for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Or is there some reason that's impossible here?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See |
||
this.insertNodesAt(sourceFile, lparen.getStart(sourceFile), [createToken(SyntaxKind.FunctionKeyword), createIdentifier(name)], { joiner: " " }); | ||
this.deleteNode(sourceFile, arrow); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this destroy any trivia? Does it matter? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will definitely clobber trivia. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
|
||
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. | ||
|
@@ -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+/, ""); | ||
|
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(); }`, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this clobber trivia on the LHS of the assignment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#23373