Skip to content

Commit f4b99ea

Browse files
authored
preserve this when extracting functions (#41992)
* preserve this when extracting functions * rename IsThisReferringToFunction to UsesThisInFunction * refactor * update tests
1 parent ca96d2e commit f4b99ea

File tree

5 files changed

+180
-5
lines changed

5 files changed

+180
-5
lines changed

src/services/refactors/extractSymbol.ts

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ namespace ts.refactor.extractSymbol {
217217
export const cannotAccessVariablesFromNestedScopes = createMessage("Cannot access variables from nested scopes");
218218
export const cannotExtractToJSClass = createMessage("Cannot extract constant to a class scope in JS");
219219
export const cannotExtractToExpressionArrowFunction = createMessage("Cannot extract constant to an arrow function without a block");
220+
export const cannotExtractFunctionsContainingThisToMethod = createMessage("Cannot extract functions containing this to method");
220221
}
221222

222223
enum RangeFacts {
@@ -225,10 +226,11 @@ namespace ts.refactor.extractSymbol {
225226
IsGenerator = 1 << 1,
226227
IsAsyncFunction = 1 << 2,
227228
UsesThis = 1 << 3,
229+
UsesThisInFunction = 1 << 4,
228230
/**
229231
* The range is in a function which needs the 'static' modifier in a class
230232
*/
231-
InStaticRegion = 1 << 4
233+
InStaticRegion = 1 << 5,
232234
}
233235

234236
/**
@@ -242,6 +244,10 @@ namespace ts.refactor.extractSymbol {
242244
* Used to ensure we don't turn something used outside the range free (or worse, resolve to a different entity).
243245
*/
244246
readonly declarations: Symbol[];
247+
/**
248+
* If `this` is referring to a function instead of class, we need to retrieve its type.
249+
*/
250+
readonly thisNode: Node | undefined;
245251
}
246252

247253
/**
@@ -294,6 +300,8 @@ namespace ts.refactor.extractSymbol {
294300
// about what things need to be done as part of the extraction.
295301
let rangeFacts = RangeFacts.None;
296302

303+
let thisNode: Node | undefined;
304+
297305
if (!start || !end) {
298306
// cannot find either start or end node
299307
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] };
@@ -336,7 +344,7 @@ namespace ts.refactor.extractSymbol {
336344
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] };
337345
}
338346

339-
return { targetRange: { range: statements, facts: rangeFacts, declarations } };
347+
return { targetRange: { range: statements, facts: rangeFacts, declarations, thisNode } };
340348
}
341349

342350
if (isReturnStatement(start) && !start.expression) {
@@ -351,7 +359,7 @@ namespace ts.refactor.extractSymbol {
351359
if (errors) {
352360
return { errors };
353361
}
354-
return { targetRange: { range: getStatementOrExpressionRange(node)!, facts: rangeFacts, declarations } }; // TODO: GH#18217
362+
return { targetRange: { range: getStatementOrExpressionRange(node)!, facts: rangeFacts, declarations, thisNode } }; // TODO: GH#18217
355363

356364
/**
357365
* Attempt to refine the extraction node (generally, by shrinking it) to produce better results.
@@ -453,6 +461,17 @@ namespace ts.refactor.extractSymbol {
453461

454462
visit(nodeToCheck);
455463

464+
if (rangeFacts & RangeFacts.UsesThis) {
465+
const container = getThisContainer(nodeToCheck, /** includeArrowFunctions */ false);
466+
if (
467+
container.kind === SyntaxKind.FunctionDeclaration ||
468+
(container.kind === SyntaxKind.MethodDeclaration && container.parent.kind === SyntaxKind.ObjectLiteralExpression) ||
469+
container.kind === SyntaxKind.FunctionExpression
470+
) {
471+
rangeFacts |= RangeFacts.UsesThisInFunction;
472+
}
473+
}
474+
456475
return errors;
457476

458477
function visit(node: Node) {
@@ -494,13 +513,15 @@ namespace ts.refactor.extractSymbol {
494513
}
495514
else {
496515
rangeFacts |= RangeFacts.UsesThis;
516+
thisNode = node;
497517
}
498518
break;
499519
case SyntaxKind.ArrowFunction:
500520
// check if arrow function uses this
501521
forEachChild(node, function check(n) {
502522
if (isThis(n)) {
503523
rangeFacts |= RangeFacts.UsesThis;
524+
thisNode = node;
504525
}
505526
else if (isClassLike(n) || (isFunctionLike(n) && !isArrowFunction(n))) {
506527
return false;
@@ -559,6 +580,7 @@ namespace ts.refactor.extractSymbol {
559580
case SyntaxKind.ThisType:
560581
case SyntaxKind.ThisKeyword:
561582
rangeFacts |= RangeFacts.UsesThis;
583+
thisNode = node;
562584
break;
563585
case SyntaxKind.LabeledStatement: {
564586
const label = (node as LabeledStatement).label;
@@ -650,7 +672,7 @@ namespace ts.refactor.extractSymbol {
650672
*/
651673
function collectEnclosingScopes(range: TargetRange): Scope[] {
652674
let current: Node = isReadonlyArray(range.range) ? first(range.range) : range.range;
653-
if (range.facts & RangeFacts.UsesThis) {
675+
if (range.facts & RangeFacts.UsesThis && !(range.facts & RangeFacts.UsesThisInFunction)) {
654676
// if range uses this as keyword or as type inside the class then it can only be extracted to a method of the containing class
655677
const containingClass = getContainingClass(current);
656678
if (containingClass) {
@@ -904,6 +926,8 @@ namespace ts.refactor.extractSymbol {
904926

905927
let newFunction: MethodDeclaration | FunctionDeclaration;
906928

929+
const callThis = !!(range.facts & RangeFacts.UsesThisInFunction);
930+
907931
if (isClassLike(scope)) {
908932
// always create private method in TypeScript files
909933
const modifiers: Modifier[] = isJS ? [] : [factory.createModifier(SyntaxKind.PrivateKeyword)];
@@ -926,6 +950,23 @@ namespace ts.refactor.extractSymbol {
926950
);
927951
}
928952
else {
953+
if (callThis) {
954+
parameters.unshift(
955+
factory.createParameterDeclaration(
956+
/*decorators*/ undefined,
957+
/*modifiers*/ undefined,
958+
/*dotDotDotToken*/ undefined,
959+
/*name*/ "this",
960+
/*questionToken*/ undefined,
961+
checker.typeToTypeNode(
962+
checker.getTypeAtLocation(range.thisNode!),
963+
scope,
964+
NodeBuilderFlags.NoTruncation
965+
),
966+
/*initializer*/ undefined,
967+
)
968+
);
969+
}
929970
newFunction = factory.createFunctionDeclaration(
930971
/*decorators*/ undefined,
931972
range.facts & RangeFacts.IsAsyncFunction ? [factory.createToken(SyntaxKind.AsyncKeyword)] : undefined,
@@ -953,8 +994,15 @@ namespace ts.refactor.extractSymbol {
953994
// replace range with function call
954995
const called = getCalledExpression(scope, range, functionNameText);
955996

997+
if (callThis) {
998+
callArguments.unshift(factory.createIdentifier("this"));
999+
}
1000+
9561001
let call: Expression = factory.createCallExpression(
957-
called,
1002+
callThis ? factory.createPropertyAccessExpression(
1003+
called,
1004+
"call"
1005+
) : called,
9581006
callTypeArguments, // Note that no attempt is made to take advantage of type argument inference
9591007
callArguments);
9601008
if (range.facts & RangeFacts.IsGenerator) {
@@ -1710,6 +1758,10 @@ namespace ts.refactor.extractSymbol {
17101758
constantErrorsPerScope[i].push(createDiagnosticForNode(errorNode, Messages.cannotAccessVariablesFromNestedScopes));
17111759
}
17121760

1761+
if (targetRange.facts & RangeFacts.UsesThisInFunction && isClassLike(scopes[i])) {
1762+
functionErrorsPerScope[i].push(createDiagnosticForNode(targetRange.thisNode!, Messages.cannotExtractFunctionsContainingThisToMethod));
1763+
}
1764+
17131765
let hasWrite = false;
17141766
let readonlyClassPropertyWrite: Declaration | undefined;
17151767
usagesPerScope[i].usages.forEach(value => {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
4+
////function test(this: string, foo: string) {
5+
//// /*start*/console.log(this);
6+
//// console.log(foo); /*end*/
7+
////}
8+
9+
goTo.select("start", "end");
10+
verify.refactorAvailable("Extract Symbol", "function_scope_0");
11+
12+
goTo.select("start", "end");
13+
edit.applyRefactor({
14+
refactorName: "Extract Symbol",
15+
actionName: "function_scope_1",
16+
actionDescription: "Extract to function in global scope",
17+
newContent:
18+
`function test(this: string, foo: string) {
19+
/*RENAME*/newFunction.call(this, foo);
20+
}
21+
22+
function newFunction(this: string, foo: string) {
23+
console.log(this);
24+
console.log(foo);
25+
}
26+
`
27+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////function test(this: { bar: string }, foo: string) {
4+
//// /*start*/console.log(this);
5+
//// console.log(foo); /*end*/
6+
////}
7+
8+
goTo.select("start", "end");
9+
verify.refactorAvailable("Extract Symbol", "function_scope_0");
10+
11+
goTo.select("start", "end");
12+
edit.applyRefactor({
13+
refactorName: "Extract Symbol",
14+
actionName: "function_scope_1",
15+
actionDescription: "Extract to function in global scope",
16+
newContent:
17+
`function test(this: { bar: string }, foo: string) {
18+
/*RENAME*/newFunction.call(this, foo);
19+
}
20+
21+
function newFunction(this: { bar: string; }, foo: string) {
22+
console.log(this);
23+
console.log(foo);
24+
}
25+
`
26+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
4+
////const foo = {
5+
//// bar: "1",
6+
//// baz() {
7+
//// /*start*/console.log(this);
8+
//// console.log(this.bar);/*end*/
9+
//// }
10+
////}
11+
12+
goTo.select("start", "end");
13+
verify.refactorAvailable("Extract Symbol", "function_scope_0");
14+
verify.refactorAvailable("Extract Symbol", "function_scope_1");
15+
16+
goTo.select("start", "end");
17+
edit.applyRefactor({
18+
refactorName: "Extract Symbol",
19+
actionName: "function_scope_1",
20+
actionDescription: "Extract to function in global scope",
21+
newContent:
22+
`const foo = {
23+
bar: "1",
24+
baz() {
25+
/*RENAME*/newFunction.call(this);
26+
}
27+
}
28+
29+
function newFunction(this: any) {
30+
console.log(this);
31+
console.log(this.bar);
32+
}
33+
`
34+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////class Foo {
4+
//// bar() {
5+
//// function test(this: string, foo: string) {
6+
//// /*start*/console.log(this);
7+
//// console.log(foo); /*end*/
8+
//// }
9+
//// }
10+
////}
11+
12+
13+
goTo.select("start", "end");
14+
verify.refactorAvailable("Extract Symbol", "function_scope_1");
15+
verify.not.refactorAvailable("Extract Symbol", "function_scope_2");
16+
verify.refactorAvailable("Extract Symbol", "function_scope_3");
17+
18+
edit.applyRefactor({
19+
refactorName: "Extract Symbol",
20+
actionName: "function_scope_0",
21+
actionDescription: "Extract to inner function in function 'test'",
22+
newContent:
23+
`class Foo {
24+
bar() {
25+
function test(this: string, foo: string) {
26+
/*RENAME*/newFunction.call(this);
27+
28+
29+
function newFunction(this: string) {
30+
console.log(this);
31+
console.log(foo);
32+
}
33+
}
34+
}
35+
}`
36+
});

0 commit comments

Comments
 (0)