Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

preserve this when extracting functions #41992

Merged
merged 6 commits into from
Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 68 additions & 11 deletions src/services/refactors/extractSymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ namespace ts.refactor.extractSymbol {
export const cannotExtractToOtherFunctionLike = createMessage("Cannot extract method to a function-like scope that is not a function");
export const cannotExtractToJSClass = createMessage("Cannot extract constant to a class scope in JS");
export const cannotExtractToExpressionArrowFunction = createMessage("Cannot extract constant to an arrow function without a block");
export const cannotExtractFunctionsContainingThisToMethod = createMessage("Cannot extract functions containing this to method");
}

enum RangeFacts {
Expand All @@ -202,10 +203,11 @@ namespace ts.refactor.extractSymbol {
IsGenerator = 1 << 1,
IsAsyncFunction = 1 << 2,
UsesThis = 1 << 3,
IsThisReferringToFunction = 1 << 4,
Zzzen marked this conversation as resolved.
Show resolved Hide resolved
/**
* The range is in a function which needs the 'static' modifier in a class
*/
InStaticRegion = 1 << 4
InStaticRegion = 1 << 5,
}

/**
Expand All @@ -219,6 +221,10 @@ namespace ts.refactor.extractSymbol {
* Used to ensure we don't turn something used outside the range free (or worse, resolve to a different entity).
*/
readonly declarations: Symbol[];
/**
* If `this` is referring to a function instead of class, we need to retrieve its type.
*/
readonly thisNode: Node | undefined;
}

/**
Expand Down Expand Up @@ -265,6 +271,8 @@ namespace ts.refactor.extractSymbol {
// about what things need to be done as part of the extraction.
let rangeFacts = RangeFacts.None;

let thisNode: Node | undefined;

if (!start || !end) {
// cannot find either start or end node
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] };
Expand Down Expand Up @@ -304,7 +312,7 @@ namespace ts.refactor.extractSymbol {
return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] };
}

return { targetRange: { range: statements, facts: rangeFacts, declarations } };
return { targetRange: { range: statements, facts: rangeFacts, declarations, thisNode } };
}

if (isJSDoc(start)) {
Expand All @@ -323,7 +331,7 @@ namespace ts.refactor.extractSymbol {
if (errors) {
return { errors };
}
return { targetRange: { range: getStatementOrExpressionRange(node)!, facts: rangeFacts, declarations } }; // TODO: GH#18217
return { targetRange: { range: getStatementOrExpressionRange(node)!, facts: rangeFacts, declarations, thisNode } }; // TODO: GH#18217

/**
* Attempt to refine the extraction node (generally, by shrinking it) to produce better results.
Expand Down Expand Up @@ -425,6 +433,17 @@ namespace ts.refactor.extractSymbol {

visit(nodeToCheck);

if (rangeFacts & RangeFacts.UsesThis) {
const container = getThisContainer(nodeToCheck, /** includeArrowFunctions */ false);
if (
container.kind === SyntaxKind.FunctionDeclaration ||
(container.kind === SyntaxKind.MethodDeclaration && container.parent.kind === SyntaxKind.ObjectLiteralExpression) ||
container.kind === SyntaxKind.FunctionExpression
) {
rangeFacts |= RangeFacts.IsThisReferringToFunction;
}
}

return errors;

function visit(node: Node) {
Expand Down Expand Up @@ -466,13 +485,15 @@ namespace ts.refactor.extractSymbol {
}
else {
rangeFacts |= RangeFacts.UsesThis;
thisNode = node;
}
break;
case SyntaxKind.ArrowFunction:
// check if arrow function uses this
forEachChild(node, function check(n) {
if (isThis(n)) {
rangeFacts |= RangeFacts.UsesThis;
thisNode = node;
}
else if (isClassLike(n) || (isFunctionLike(n) && !isArrowFunction(n))) {
return false;
Expand Down Expand Up @@ -531,6 +552,7 @@ namespace ts.refactor.extractSymbol {
case SyntaxKind.ThisType:
case SyntaxKind.ThisKeyword:
rangeFacts |= RangeFacts.UsesThis;
thisNode = node;
break;
case SyntaxKind.LabeledStatement: {
const label = (<LabeledStatement>node).label;
Expand Down Expand Up @@ -606,13 +628,18 @@ namespace ts.refactor.extractSymbol {
function collectEnclosingScopes(range: TargetRange): Scope[] {
let current: Node = isReadonlyArray(range.range) ? first(range.range) : range.range;
if (range.facts & RangeFacts.UsesThis) {
// 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
const containingClass = getContainingClass(current);
if (containingClass) {
const containingFunction = findAncestor(current, isFunctionLikeDeclaration);
return containingFunction
? [containingFunction, containingClass]
: [containingClass];
if (range.facts & RangeFacts.IsThisReferringToFunction) {
// fall through
}
else {
// 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
const containingClass = getContainingClass(current);
if (containingClass) {
const containingFunction = findAncestor(current, isFunctionLikeDeclaration);
return containingFunction
? [containingFunction, containingClass]
: [containingClass];
}
}
}

Expand Down Expand Up @@ -859,6 +886,8 @@ namespace ts.refactor.extractSymbol {

let newFunction: MethodDeclaration | FunctionDeclaration;

const callThis = !!(range.facts & RangeFacts.IsThisReferringToFunction);

if (isClassLike(scope)) {
// always create private method in TypeScript files
const modifiers: Modifier[] = isJS ? [] : [factory.createModifier(SyntaxKind.PrivateKeyword)];
Expand All @@ -881,6 +910,23 @@ namespace ts.refactor.extractSymbol {
);
}
else {
if (callThis) {
parameters.unshift(
factory.createParameterDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
/*dotDotDotToken*/ undefined,
/*name*/ "this",
/*questionToken*/ undefined,
checker.typeToTypeNode(
checker.getTypeAtLocation(range.thisNode!),
scope,
NodeBuilderFlags.NoTruncation
),
/*initializer*/ undefined,
)
);
}
newFunction = factory.createFunctionDeclaration(
/*decorators*/ undefined,
range.facts & RangeFacts.IsAsyncFunction ? [factory.createToken(SyntaxKind.AsyncKeyword)] : undefined,
Expand Down Expand Up @@ -908,8 +954,15 @@ namespace ts.refactor.extractSymbol {
// replace range with function call
const called = getCalledExpression(scope, range, functionNameText);

if (callThis) {
callArguments.unshift(factory.createIdentifier("this"));
}

let call: Expression = factory.createCallExpression(
called,
callThis ? factory.createPropertyAccessExpression(
called,
"call"
) : called,
callTypeArguments, // Note that no attempt is made to take advantage of type argument inference
callArguments);
if (range.facts & RangeFacts.IsGenerator) {
Expand Down Expand Up @@ -1666,6 +1719,10 @@ namespace ts.refactor.extractSymbol {
constantErrorsPerScope[i].push(createDiagnosticForNode(errorNode, Messages.cannotAccessVariablesFromNestedScopes));
}

if (targetRange.facts & RangeFacts.IsThisReferringToFunction && isClassLike(scopes[i])) {
functionErrorsPerScope[i].push(createDiagnosticForNode(targetRange.thisNode!, Messages.cannotExtractFunctionsContainingThisToMethod));
}

let hasWrite = false;
let readonlyClassPropertyWrite: Declaration | undefined;
usagesPerScope[i].usages.forEach(value => {
Expand Down
27 changes: 27 additions & 0 deletions tests/cases/fourslash/extractFunctionContainingThis1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/// <reference path='fourslash.ts' />


////function test(this: string, foo: string) {
//// /*start*/console.log(this);
//// console.log(foo); /*end*/
////}

goTo.select("start", "end");
verify.refactorAvailable("Extract Symbol", "function_scope_0");

goTo.select("start", "end");
edit.applyRefactor({
refactorName: "Extract Symbol",
actionName: "function_scope_1",
actionDescription: "Extract to function in global scope",
newContent:
`function test(this: string, foo: string) {
/*RENAME*/newFunction.call(this, foo);
}

function newFunction(this: string, foo: string) {
console.log(this);
console.log(foo);
}
`
});
26 changes: 26 additions & 0 deletions tests/cases/fourslash/extractFunctionContainingThis2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/// <reference path='fourslash.ts' />

////function test(this: { bar: string }, foo: string) {
//// /*start*/console.log(this);
//// console.log(foo); /*end*/
////}

goTo.select("start", "end");
verify.refactorAvailable("Extract Symbol", "function_scope_0");

goTo.select("start", "end");
edit.applyRefactor({
refactorName: "Extract Symbol",
actionName: "function_scope_1",
actionDescription: "Extract to function in global scope",
newContent:
`function test(this: { bar: string }, foo: string) {
/*RENAME*/newFunction.call(this, foo);
}

function newFunction(this: { bar: string; }, foo: string) {
console.log(this);
console.log(foo);
}
`
});
34 changes: 34 additions & 0 deletions tests/cases/fourslash/extractFunctionContainingThis3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/// <reference path='fourslash.ts' />


////const foo = {
//// bar: "1",
//// baz() {
//// /*start*/console.log(this);
//// console.log(this.bar);/*end*/
//// }
////}

goTo.select("start", "end");
verify.not.refactorAvailable("Extract Symbol", "function_scope_0");
verify.refactorAvailable("Extract Symbol", "function_scope_1");

goTo.select("start", "end");
edit.applyRefactor({
refactorName: "Extract Symbol",
actionName: "function_scope_1",
actionDescription: "Extract to function in global scope",
newContent:
`const foo = {
bar: "1",
baz() {
/*RENAME*/newFunction.call(this);
}
}

function newFunction(this: any) {
console.log(this);
console.log(this.bar);
}
`
});
37 changes: 37 additions & 0 deletions tests/cases/fourslash/extractFunctionContainingThis5.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/// <reference path='fourslash.ts' />

////class Foo {
//// bar() {
//// function test(this: string, foo: string) {
//// /*start*/console.log(this);
//// console.log(foo); /*end*/
//// }
//// }
////}


goTo.select("start", "end");
// cannot extract it to method, reason: Cannot extract method to a function-like scope that is not a function
verify.not.refactorAvailable("Extract Symbol", "function_scope_1");
verify.not.refactorAvailable("Extract Symbol", "function_scope_2");
verify.refactorAvailable("Extract Symbol", "function_scope_3");

edit.applyRefactor({
refactorName: "Extract Symbol",
actionName: "function_scope_0",
actionDescription: "Extract to inner function in function 'test'",
newContent:
`class Foo {
bar() {
function test(this: string, foo: string) {
/*RENAME*/newFunction.call(this);


function newFunction(this: string) {
console.log(this);
console.log(foo);
}
}
}
}`
});