Skip to content

Commit 5be0d71

Browse files
committed
Fix bug due to sharing of .type.members
Function extraction failed when using two identical `TypeLiteral`s --- the problem is that the parameters' `.type.members` are shared in their two occurences in the resulting code, and when the formatter bangs position properties on the second, it's actually changing the first too. * `typeToAutoImportableTypeNode`: wrap in `getSynthesizedDeepClone` to avoid the offeding sharing. * `getSynthesizedDeepCloneWorker`: add optional nodes/token visitors so it doesn't skip cloning the above. * A few very minor tweaks which I saw when tracing this (comment update, two `!`s). Fixes #44301
1 parent a875635 commit 5be0d71

File tree

4 files changed

+34
-11
lines changed

4 files changed

+34
-11
lines changed

src/services/codefixes/helpers.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ namespace ts.codefix {
127127
}
128128

129129
for (const signature of signatures) {
130-
// Need to ensure nodes are fresh each time so they can have different positions.
130+
// Ensure nodes are fresh so they can have different positions when going through formatting.
131131
outputMethod(quotePreference, signature, getSynthesizedDeepClones(modifiers, /*includeTrivia*/ false), getSynthesizedDeepClone(name, /*includeTrivia*/ false));
132132
}
133133

@@ -310,15 +310,16 @@ namespace ts.codefix {
310310
}
311311

312312
export function typeToAutoImportableTypeNode(checker: TypeChecker, importAdder: ImportAdder, type: Type, contextNode: Node | undefined, scriptTarget: ScriptTarget, flags?: NodeBuilderFlags, tracker?: SymbolTracker): TypeNode | undefined {
313-
const typeNode = checker.typeToTypeNode(type, contextNode, flags, tracker);
313+
let typeNode = checker.typeToTypeNode(type, contextNode, flags, tracker);
314314
if (typeNode && isImportTypeNode(typeNode)) {
315315
const importableReference = tryGetAutoImportableReferenceFromTypeNode(typeNode, scriptTarget);
316316
if (importableReference) {
317317
importSymbols(importAdder, importableReference.symbols);
318-
return importableReference.typeNode;
318+
typeNode = importableReference.typeNode;
319319
}
320320
}
321-
return typeNode;
321+
// Ensure nodes are fresh so they can have different positions when going through formatting.
322+
return getSynthesizedDeepClone(typeNode);
322323
}
323324

324325
function createDummyParameters(argCount: number, names: (string | undefined)[] | undefined, types: (TypeNode | undefined)[] | undefined, minArgumentCount: number | undefined, inJs: boolean): ParameterDeclaration[] {

src/services/textChanges.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -991,7 +991,7 @@ namespace ts.textChanges {
991991
const { options = {}, range: { pos } } = change;
992992
const format = (n: Node) => getFormattedTextOfNode(n, sourceFile, pos, options, newLineCharacter, formatContext, validate);
993993
const text = change.kind === ChangeKind.ReplaceWithMultipleNodes
994-
? change.nodes.map(n => removeSuffix(format(n), newLineCharacter)).join(change.options!.joiner || newLineCharacter) // TODO: GH#18217
994+
? change.nodes.map(n => removeSuffix(format(n), newLineCharacter)).join(change.options?.joiner || newLineCharacter)
995995
: format(change.node);
996996
// strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line
997997
const noIndent = (options.preserveLeadingWhitespace || options.indentation !== undefined || getLineStartPositionForPosition(pos, sourceFile) === pos) ? text : text.replace(/^\s+/, "");
@@ -1054,7 +1054,7 @@ namespace ts.textChanges {
10541054
}
10551055

10561056
function assignPositionsToNode(node: Node): Node {
1057-
const visited = visitEachChild(node, assignPositionsToNode, nullTransformationContext, assignPositionsToNodeArray, assignPositionsToNode)!; // TODO: GH#18217
1057+
const visited = visitEachChild(node, assignPositionsToNode, nullTransformationContext, assignPositionsToNodeArray, assignPositionsToNode);
10581058
// create proxy node for non synthesized nodes
10591059
const newNode = nodeIsSynthesized(visited) ? visited : Object.create(visited) as Node;
10601060
setTextRangePosEnd(newNode, getPos(node), getEnd(node));

src/services/utilities.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2377,9 +2377,14 @@ namespace ts {
23772377
}
23782378

23792379
function getSynthesizedDeepCloneWorker<T extends Node>(node: T, replaceNode?: (node: Node) => Node | undefined): T {
2380-
const visited = replaceNode ?
2381-
visitEachChild(node, n => getSynthesizedDeepCloneWithReplacements(n, /*includeTrivia*/ true, replaceNode), nullTransformationContext) :
2382-
visitEachChild(node, getSynthesizedDeepClone, nullTransformationContext);
2380+
const nodeClone: (n: T) => T = replaceNode
2381+
? n => getSynthesizedDeepCloneWithReplacements(n, /*includeTrivia*/ true, replaceNode)
2382+
: getSynthesizedDeepClone;
2383+
const nodesClone: (ns: NodeArray<T>) => NodeArray<T> = replaceNode
2384+
? ns => ns && getSynthesizedDeepClonesWithReplacements(ns, /*includeTrivia*/ true, replaceNode)
2385+
: ns => ns && getSynthesizedDeepClones(ns);
2386+
const visited =
2387+
visitEachChild(node, nodeClone, nullTransformationContext, nodesClone, nodeClone);
23832388

23842389
if (visited === node) {
23852390
// This only happens for leaf nodes - internal nodes always see their children change.
@@ -2390,8 +2395,8 @@ namespace ts {
23902395
return setTextRange(clone, node);
23912396
}
23922397

2393-
// PERF: As an optimization, rather than calling getSynthesizedClone, we'll update
2394-
// the new node created by visitEachChild with the extra changes getSynthesizedClone
2398+
// PERF: As an optimization, rather than calling factory.cloneNode, we'll update
2399+
// the new node created by visitEachChild with the extra changes factory.cloneNode
23952400
// would have made.
23962401
(visited as Mutable<T>).parent = undefined!;
23972402
return visited;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////(x: {}, y: {}) => (/*1*/x + y/*2*/);
4+
5+
goTo.select("1", "2");
6+
edit.applyRefactor({
7+
refactorName: "Extract Symbol",
8+
actionName: "function_scope_1",
9+
actionDescription: "Extract to function in global scope",
10+
newContent:
11+
`(x: {}, y: {}) => (/*RENAME*/newFunction(x, y));
12+
13+
function newFunction(x: {}, y: {}) {
14+
return x + y;
15+
}
16+
`
17+
});

0 commit comments

Comments
 (0)