Skip to content

Commit fb9b3fe

Browse files
author
Orta
authored
Merge pull request #32345 from dragomirtitian/GH-32325-improve-const-function-extraction
Improved const extraction for function expressions and arrow functions
2 parents 7435425 + 1d97ae6 commit fb9b3fe

12 files changed

+224
-3
lines changed

src/services/refactors/extractSymbol.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,11 +1006,14 @@ namespace ts.refactor.extractSymbol {
10061006
const localNameText = getUniqueName(isClassLike(scope) ? "newProperty" : "newLocal", file);
10071007
const isJS = isInJSFile(scope);
10081008

1009-
const variableType = isJS || !checker.isContextSensitive(node)
1009+
let variableType = isJS || !checker.isContextSensitive(node)
10101010
? undefined
10111011
: checker.typeToTypeNode(checker.getContextualType(node)!, scope, NodeBuilderFlags.NoTruncation); // TODO: GH#18217
10121012

1013-
const initializer = transformConstantInitializer(node, substitutions);
1013+
let initializer = transformConstantInitializer(node, substitutions);
1014+
1015+
({ variableType, initializer } = transformFunctionInitializerAndType(variableType, initializer));
1016+
10141017
suppressLeadingAndTrailingTrivia(initializer);
10151018

10161019
const changeTracker = textChanges.ChangeTracker.fromContext(context);
@@ -1102,6 +1105,73 @@ namespace ts.refactor.extractSymbol {
11021105
const renameFilename = node.getSourceFile().fileName;
11031106
const renameLocation = getRenameLocation(edits, renameFilename, localNameText, /*isDeclaredBeforeUse*/ true);
11041107
return { renameFilename, renameLocation, edits };
1108+
1109+
function transformFunctionInitializerAndType(variableType: TypeNode | undefined, initializer: Expression): { variableType: TypeNode | undefined, initializer: Expression } {
1110+
// If no contextual type exists there is nothing to transfer to the function signature
1111+
if (variableType === undefined) return { variableType, initializer };
1112+
// Only do this for function expressions and arrow functions that are not generic
1113+
if (!isFunctionExpression(initializer) && !isArrowFunction(initializer) || !!initializer.typeParameters) return { variableType, initializer };
1114+
const functionType = checker.getTypeAtLocation(node);
1115+
const functionSignature = singleOrUndefined(checker.getSignaturesOfType(functionType, SignatureKind.Call));
1116+
1117+
// If no function signature, maybe there was an error, do nothing
1118+
if (!functionSignature) return { variableType, initializer };
1119+
// If the function signature has generic type parameters we don't attempt to move the parameters
1120+
if (!!functionSignature.getTypeParameters()) return { variableType, initializer };
1121+
1122+
// We add parameter types if needed
1123+
const parameters: ParameterDeclaration[] = [];
1124+
let hasAny = false;
1125+
for (const p of initializer.parameters) {
1126+
if (p.type) {
1127+
parameters.push(p);
1128+
}
1129+
else {
1130+
const paramType = checker.getTypeAtLocation(p);
1131+
if (paramType === checker.getAnyType()) hasAny = true;
1132+
1133+
parameters.push(updateParameter(p,
1134+
p.decorators, p.modifiers, p.dotDotDotToken,
1135+
p.name, p.questionToken, p.type || checker.typeToTypeNode(paramType, scope, NodeBuilderFlags.NoTruncation), p.initializer));
1136+
}
1137+
}
1138+
// If a parameter was inferred as any we skip adding function parameters at all.
1139+
// Turning an implicit any (which under common settings is a error) to an explicit
1140+
// is probably actually a worse refactor outcome.
1141+
if (hasAny) return { variableType, initializer };
1142+
variableType = undefined;
1143+
if (isArrowFunction(initializer)) {
1144+
initializer = updateArrowFunction(initializer, node.modifiers, initializer.typeParameters,
1145+
parameters,
1146+
initializer.type || checker.typeToTypeNode(functionSignature.getReturnType(), scope, NodeBuilderFlags.NoTruncation),
1147+
initializer.equalsGreaterThanToken,
1148+
initializer.body);
1149+
}
1150+
else {
1151+
if (functionSignature && !!functionSignature.thisParameter) {
1152+
const firstParameter = firstOrUndefined(parameters);
1153+
// If the function signature has a this parameter and if the first defined parameter is not the this parameter, we must add it
1154+
// Note: If this parameter was already there, it would have been previously updated with the type if not type was present
1155+
if ((!firstParameter || (isIdentifier(firstParameter.name) && firstParameter.name.escapedText !== "this"))) {
1156+
const thisType = checker.getTypeOfSymbolAtLocation(functionSignature.thisParameter, node);
1157+
parameters.splice(0, 0, createParameter(
1158+
/* decorators */ undefined,
1159+
/* modifiers */ undefined,
1160+
/* dotDotDotToken */ undefined,
1161+
"this",
1162+
/* questionToken */ undefined,
1163+
checker.typeToTypeNode(thisType, scope, NodeBuilderFlags.NoTruncation)
1164+
));
1165+
}
1166+
}
1167+
initializer = updateFunctionExpression(initializer, node.modifiers, initializer.asteriskToken,
1168+
initializer.name, initializer.typeParameters,
1169+
parameters,
1170+
initializer.type || checker.typeToTypeNode(functionSignature.getReturnType(), scope, NodeBuilderFlags.NoTruncation),
1171+
initializer.body);
1172+
}
1173+
return { variableType, initializer };
1174+
}
11051175
}
11061176

11071177
function getContainingVariableDeclarationIfInList(node: Node, scope: Scope) {

tests/baselines/reference/extractConstant/extractConstant_ContextualType_Lambda.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const myObj: { member(x: number, y: string): void } = {
55
}
66

77
// ==SCOPE::Extract to constant in enclosing scope==
8-
const newLocal: (x: number, y: string) => void = (x, y) => x + y;
8+
const newLocal = (x: number, y: string): string => x + y;
99
const myObj: { member(x: number, y: string): void } = {
1010
member: /*RENAME*/newLocal,
1111
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////declare function fnUnion(fn: <T>(a: T) => T): void
4+
////fnUnion(/*a*/a => a/*b*/);
5+
6+
goTo.select("a", "b");
7+
edit.applyRefactor({
8+
refactorName: "Extract Symbol",
9+
actionName: "constant_scope_0",
10+
actionDescription: "Extract to constant in enclosing scope",
11+
newContent:
12+
`declare function fnUnion(fn: <T>(a: T) => T): void
13+
const newLocal: <T>(a: T) => T = a => a;
14+
fnUnion(/*RENAME*/newLocal);`
15+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////declare function fnUnion(fn: ((a: number) => number) | ((a: string) => string)): void
4+
////fnUnion(/*a*/(a: any) => a/*b*/);
5+
6+
goTo.select("a", "b");
7+
edit.applyRefactor({
8+
refactorName: "Extract Symbol",
9+
actionName: "constant_scope_0",
10+
actionDescription: "Extract to constant in enclosing scope",
11+
newContent:
12+
`declare function fnUnion(fn: ((a: number) => number) | ((a: string) => string)): void
13+
const newLocal = (a: any) => a;
14+
fnUnion(/*RENAME*/newLocal);`
15+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////declare function fnUnion(fn: ((a: number) => number) | ((a: string) => string)): void
4+
////fnUnion(/*a*/(a) => a/*b*/);
5+
6+
goTo.select("a", "b");
7+
edit.applyRefactor({
8+
refactorName: "Extract Symbol",
9+
actionName: "constant_scope_0",
10+
actionDescription: "Extract to constant in enclosing scope",
11+
newContent:
12+
`declare function fnUnion(fn: ((a: number) => number) | ((a: string) => string)): void
13+
const newLocal: ((a: number) => number) | ((a: string) => string) = (a) => a;
14+
fnUnion(/*RENAME*/newLocal);`
15+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////declare function fnUnion(fn: ((a: number) => number) | ((a: string) => string)): void
4+
////fnUnion(/*a*/(a: string) => a/*b*/);
5+
6+
goTo.select("a", "b");
7+
edit.applyRefactor({
8+
refactorName: "Extract Symbol",
9+
actionName: "constant_scope_0",
10+
actionDescription: "Extract to constant in enclosing scope",
11+
newContent:
12+
`declare function fnUnion(fn: ((a: number) => number) | ((a: string) => string)): void
13+
const newLocal = (a: string) => a;
14+
fnUnion(/*RENAME*/newLocal);`
15+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
4+
////function fWithRest(fn: (...a: number[]) => number) { }
5+
////fWithRest(/*a*/(a, b, c) => a + b + c/*b*/);
6+
7+
8+
goTo.select("a", "b");
9+
edit.applyRefactor({
10+
refactorName: "Extract Symbol",
11+
actionName: "constant_scope_0",
12+
actionDescription: "Extract to constant in enclosing scope",
13+
newContent:
14+
`function fWithRest(fn: (...a: number[]) => number) { }
15+
const newLocal = (a: number, b: number, c: number): number => a + b + c;
16+
fWithRest(/*RENAME*/newLocal);`
17+
});
18+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////declare function fWithThis(fn: (this: { a: string }, a: string) => string): void;
4+
////fWithThis(/*a*/function (a: string): string { return this.a; }/*b*/);
5+
6+
goTo.select("a", "b");
7+
edit.applyRefactor({
8+
refactorName: "Extract Symbol",
9+
actionName: "constant_scope_0",
10+
actionDescription: "Extract to constant in enclosing scope",
11+
newContent:
12+
`declare function fWithThis(fn: (this: { a: string }, a: string) => string): void;
13+
const newLocal = function(this: { a: string; }, a: string): string { return this.a; };
14+
fWithThis(/*RENAME*/newLocal);`
15+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////declare function fWithThis(fn: (this: { a: string }, a: string) => string): void;
4+
////fWithThis(/*a*/function (a) { return this.a; }/*b*/);
5+
6+
goTo.select("a", "b");
7+
edit.applyRefactor({
8+
refactorName: "Extract Symbol",
9+
actionName: "constant_scope_0",
10+
actionDescription: "Extract to constant in enclosing scope",
11+
newContent:
12+
`declare function fWithThis(fn: (this: { a: string }, a: string) => string): void;
13+
const newLocal = function(this: { a: string; }, a: string): string { return this.a; };
14+
fWithThis(/*RENAME*/newLocal);`
15+
});
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+
////declare function fWithThis(fn: (this: { a: string }, a: string) => string): void;
4+
////fWithThis(/*a*/function (this: { a: string }, a) { return this.a; }/*b*/);
5+
6+
goTo.select("a", "b");
7+
edit.applyRefactor({
8+
refactorName: "Extract Symbol",
9+
actionName: "constant_scope_0",
10+
actionDescription: "Extract to constant in enclosing scope",
11+
newContent:
12+
`declare function fWithThis(fn: (this: { a: string }, a: string) => string): void;
13+
const newLocal = function(this: {
14+
a: string;
15+
}, a: string): string { return this.a; };
16+
fWithThis(/*RENAME*/newLocal);`
17+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////const x = [1,2,3].map(/*a*/function (x) { return x + 1 }/*b*/);
4+
5+
goTo.select("a", "b");
6+
edit.applyRefactor({
7+
refactorName: "Extract Symbol",
8+
actionName: "constant_scope_0",
9+
actionDescription: "Extract to constant in enclosing scope",
10+
newContent:
11+
`const newLocal = function(x: number): number { return x + 1; };
12+
const x = [1,2,3].map(/*RENAME*/newLocal);`
13+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////const x = [1,2,3].map(/*a*/x => x + 1/*b*/);
4+
5+
goTo.select("a", "b");
6+
edit.applyRefactor({
7+
refactorName: "Extract Symbol",
8+
actionName: "constant_scope_0",
9+
actionDescription: "Extract to constant in enclosing scope",
10+
newContent:
11+
`const newLocal = (x: number): number => x + 1;
12+
const x = [1,2,3].map(/*RENAME*/newLocal);`
13+
});

0 commit comments

Comments
 (0)