convertToEs6Module: Avoid replacing entire function (#22507)

* convertToEs6Module: Avoid replacing entire function

* Code review

* Fix typo
This commit is contained in:
Andy
2018-04-16 13:16:04 -07:00
committed by GitHub
parent a8618a79e1
commit aac9ef5e51
8 changed files with 105 additions and 35 deletions

View File

@@ -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,12 +177,11 @@ 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;
}
@@ -190,7 +189,7 @@ namespace ts.codefix {
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;
@@ -198,12 +197,12 @@ namespace ts.codefix {
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,14 +233,12 @@ 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) {
/*
@@ -249,13 +246,13 @@ namespace ts.codefix {
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 });
}
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: {

View File

@@ -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,7 +316,7 @@ 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);
}
@@ -326,7 +330,7 @@ namespace ts.textChanges {
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 } });
}
@@ -483,6 +487,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() {}`
this.insertNodesAt(sourceFile, lparen.getStart(sourceFile), [createToken(SyntaxKind.FunctionKeyword), createIdentifier(name)], { joiner: " " });
this.deleteNode(sourceFile, arrow);
}
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.
@@ -652,7 +685,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+/, "");