diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 968144c0fc7..fe183c5e806 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -47,10 +47,15 @@ namespace ts { * Creates a shallow, memberwise clone of a node with no source map location. */ /* @internal */ - export function getSynthesizedClone(node: T | undefined): T { + export function getSynthesizedClone(node: T | undefined): T | undefined { // We don't use "clone" from core.ts here, as we need to preserve the prototype chain of // the original node. We also need to exclude specific properties and only include own- // properties (to skip members already defined on the shared prototype). + + if (node === undefined) { + return undefined; + } + const clone = createSynthesizedNode(node.kind); clone.flags |= node.flags; setOriginalNode(clone, node); diff --git a/src/services/refactors/extractSymbol.ts b/src/services/refactors/extractSymbol.ts index ba5a6d9d1b5..9defe3dd1f0 100644 --- a/src/services/refactors/extractSymbol.ts +++ b/src/services/refactors/extractSymbol.ts @@ -1088,7 +1088,7 @@ namespace ts.refactor.extractSymbol { } } - function transformFunctionBody(body: Node, writes: ReadonlyArray, substitutions: ReadonlyMap<() => Node>, hasReturn: boolean): { body: Block, returnValueProperty: string } { + function transformFunctionBody(body: Node, writes: ReadonlyArray, substitutions: ReadonlyMap, hasReturn: boolean): { body: Block, returnValueProperty: string } { if (isBlock(body) && !writes && substitutions.size === 0) { // already block, no writes to propagate back, no substitutions - can use node as is return { body: createBlock(body.statements, /*multLine*/ true), returnValueProperty: undefined }; @@ -1136,21 +1136,21 @@ namespace ts.refactor.extractSymbol { const oldIgnoreReturns = ignoreReturns; ignoreReturns = ignoreReturns || isFunctionLikeDeclaration(node) || isClassLike(node); const substitution = substitutions.get(getNodeId(node).toString()); - const result = substitution ? substitution() : visitEachChild(node, visitor, nullTransformationContext); + const result = substitution ? getSynthesizedDeepClone(substitution) : visitEachChild(node, visitor, nullTransformationContext); ignoreReturns = oldIgnoreReturns; return result; } } } - function transformConstantInitializer(initializer: Expression, substitutions: ReadonlyMap<() => Node>): Expression { + function transformConstantInitializer(initializer: Expression, substitutions: ReadonlyMap): Expression { return substitutions.size ? visitor(initializer) as Expression : initializer; function visitor(node: Node): VisitResult { const substitution = substitutions.get(getNodeId(node).toString()); - return substitution ? substitution() : visitEachChild(node, visitor, nullTransformationContext); + return substitution ? getSynthesizedDeepClone(substitution) : visitEachChild(node, visitor, nullTransformationContext); } } @@ -1279,7 +1279,7 @@ namespace ts.refactor.extractSymbol { interface ScopeUsages { readonly usages: Map; readonly typeParameterUsages: Map; // Key is type ID - readonly substitutions: Map<() => Node>; + readonly substitutions: Map; } interface ReadsAndWrites { @@ -1298,7 +1298,7 @@ namespace ts.refactor.extractSymbol { const allTypeParameterUsages = createMap(); // Key is type ID const usagesPerScope: ScopeUsages[] = []; - const substitutionsPerScope: Map<() => Node>[] = []; + const substitutionsPerScope: Map[] = []; const functionErrorsPerScope: Diagnostic[][] = []; const constantErrorsPerScope: Diagnostic[][] = []; const visibleDeclarationsInExtractedRange: Symbol[] = []; @@ -1322,8 +1322,8 @@ namespace ts.refactor.extractSymbol { // initialize results for (const scope of scopes) { - usagesPerScope.push({ usages: createMap(), typeParameterUsages: createMap(), substitutions: createMap<() => Expression>() }); - substitutionsPerScope.push(createMap<() => Expression>()); + usagesPerScope.push({ usages: createMap(), typeParameterUsages: createMap(), substitutions: createMap() }); + substitutionsPerScope.push(createMap()); functionErrorsPerScope.push( isFunctionLikeDeclaration(scope) && scope.kind !== SyntaxKind.FunctionDeclaration @@ -1622,20 +1622,20 @@ namespace ts.refactor.extractSymbol { } } - function tryReplaceWithQualifiedNameOrPropertyAccess(symbol: Symbol, scopeDecl: Node, isTypeNode: boolean): () => (PropertyAccessExpression | EntityName) { + function tryReplaceWithQualifiedNameOrPropertyAccess(symbol: Symbol, scopeDecl: Node, isTypeNode: boolean): PropertyAccessExpression | EntityName { if (!symbol) { return undefined; } if (symbol.getDeclarations().some(d => d.parent === scopeDecl)) { - return () => createIdentifier(symbol.name); + return createIdentifier(symbol.name); } const prefix = tryReplaceWithQualifiedNameOrPropertyAccess(symbol.parent, scopeDecl, isTypeNode); if (prefix === undefined) { return undefined; } return isTypeNode - ? () => createQualifiedName(prefix(), createIdentifier(symbol.name)) - : () => createPropertyAccess(prefix(), symbol.name); + ? createQualifiedName(prefix, createIdentifier(symbol.name)) + : createPropertyAccess(prefix, symbol.name); } } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 6166ceea28c..a8dea4ddd0e 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1334,4 +1334,33 @@ namespace ts { } return position; } + + /** + * Creates a deep, memberwise clone of a node with no source map location. + * + * WARNING: This is an expensive operation and is only intended to be used in refactorings + * and code fixes (because those are triggered by explicit user actions). + */ + export function getSynthesizedDeepClone(node: T | undefined): T | undefined { + if (node === undefined) { + return undefined; + } + + const visited = visitEachChild(node, getSynthesizedDeepClone, nullTransformationContext); + if (visited === node) { + // This only happens for leaf nodes - internal nodes always see their children change. + const clone = getSynthesizedClone(node); + clone.pos = node.pos; + clone.end = node.end; + return clone; + } + + // PERF: As an optimization, rather than calling getSynthesizedClone, we'll update + // the new node created by visitEachChild with the extra changes getSynthesizedClone + // would have made. + + visited.parent = undefined; + + return visited; + } }