Skip to content

Extract method #16960

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

Closed
wants to merge 60 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
be27a1a
Rebased EM WIP
RyanCavanaugh Jun 30, 2017
e798257
Change refactor api in fourslash
RyanCavanaugh Jul 5, 2017
bf176a6
Add comments
RyanCavanaugh Jul 5, 2017
0130ebb
Better baselines
RyanCavanaugh Jul 5, 2017
bbf5cd7
Rename test we we can run it independent of unit tests
RyanCavanaugh Jul 5, 2017
9dd6bef
Change fourslash refactor API
RyanCavanaugh Jul 5, 2017
f21b7ae
Remove invalid assert
RyanCavanaugh Jul 5, 2017
674b1bf
Fix undefined computation of length
RyanCavanaugh Jul 5, 2017
9367473
Improved type baselines for EM .js
RyanCavanaugh Jul 5, 2017
1ea9972
Add validation to fourslash test
RyanCavanaugh Jul 5, 2017
1787fa8
Move text to diagnostic messages
RyanCavanaugh Jul 5, 2017
fb89c00
Merge branch 'master' into extract-method
RyanCavanaugh Jul 5, 2017
4aef5e7
Properly widen literal param types + emit correct return statements f…
RyanCavanaugh Jul 5, 2017
b5c5c40
Don't offer to extract lone identifiers
RyanCavanaugh Jul 6, 2017
b1fa49f
Don't fail if span is zero-length
RyanCavanaugh Jul 6, 2017
825237b
Don't crash when span is empty
RyanCavanaugh Jul 6, 2017
aa6f3aa
Restore assert and fix synthetic type node creation
RyanCavanaugh Jul 18, 2017
b7e4451
Re-use noop
RyanCavanaugh Jul 18, 2017
419a4c2
Address portion of CR feedback
RyanCavanaugh Jul 18, 2017
4218738
Only compute changes for requested refactors
RyanCavanaugh Jul 19, 2017
6d2bc90
Address more PR comments
RyanCavanaugh Jul 19, 2017
9a24549
Merge master
RyanCavanaugh Jul 19, 2017
271ecb1
Remove WS
RyanCavanaugh Jul 19, 2017
bdefcc3
Emit return types in ctxly-typed functions and don't emit annotations…
RyanCavanaugh Jul 19, 2017
6428b1e
Fix broken test
RyanCavanaugh Jul 19, 2017
8af8ac2
Handle fn params and duplicated scope names properly
RyanCavanaugh Jul 19, 2017
ede6671
Re-use isJS result
RyanCavanaugh Jul 19, 2017
55b28ad
You may not extract-method on exported declarations
RyanCavanaugh Jul 19, 2017
2d9eaa9
Correctly apply overlapped edits in fourslash
RyanCavanaugh Jul 20, 2017
2f2c780
Fix some parenting issues & start detecting stranded references
RyanCavanaugh Jul 20, 2017
63193da
Add return type annotation and regression test for a prior assert
RyanCavanaugh Jul 20, 2017
889292a
Detect would-be stranded references to extracted declarations
RyanCavanaugh Jul 24, 2017
e229d42
Tests
RyanCavanaugh Jul 24, 2017
7b2dd8a
Fix line ending
RyanCavanaugh Jul 24, 2017
bf6c0a9
Function blocks aren't statements!
RyanCavanaugh Jul 25, 2017
3424312
Disallow certain classes of refactors
RyanCavanaugh Jul 25, 2017
ff18df3
Add test
RyanCavanaugh Jul 25, 2017
7f29fcd
Handle static methods and static method name conflicts
RyanCavanaugh Jul 25, 2017
d39d95b
Merge
RyanCavanaugh Jul 25, 2017
dda5496
No extracting other non-statement blocks
RyanCavanaugh Jul 28, 2017
e88ff21
More disallowed extraction position
RyanCavanaugh Jul 28, 2017
bd80005
Block more nonworking extraction locations
RyanCavanaugh Jul 28, 2017
ff716d0
Properly enumerate class blocks
RyanCavanaugh Jul 28, 2017
1291cfd
Add tests task
RyanCavanaugh Jul 28, 2017
a26790f
Don't re-use nodes
RyanCavanaugh Jul 28, 2017
56e1d6d
Correctly detect JavaScriptness
RyanCavanaugh Jul 28, 2017
7f3a0a8
Create multi-line bodies
RyanCavanaugh Jul 31, 2017
003cd0a
Add support for validating specific refactor actions' availability
RyanCavanaugh Aug 3, 2017
a76664c
More testcases
RyanCavanaugh Aug 3, 2017
42e0652
Accept baselines
RyanCavanaugh Aug 3, 2017
46a84c3
Innumerable bugfixes
RyanCavanaugh Aug 3, 2017
a97ab93
Merge branch 'master' into extract-method
RyanCavanaugh Aug 3, 2017
37ae957
Add additional test
RyanCavanaugh Aug 3, 2017
2adb2b3
Fix some class scenarios (static methods, readonly inits in ctor)
RyanCavanaugh Aug 4, 2017
ba37830
Prevent extraction of exported variables
RyanCavanaugh Aug 4, 2017
4dafb40
No extraction of ambient declarations
RyanCavanaugh Aug 4, 2017
c1c7e83
Put __return first
RyanCavanaugh Aug 4, 2017
a4c41b8
Don't wrongly exclude accessor in indexed access exprs
RyanCavanaugh Aug 4, 2017
b5a88c1
Share selection logic; fix typos
RyanCavanaugh Aug 4, 2017
568d5f6
Merge branch 'master' into extract-method
RyanCavanaugh Aug 4, 2017
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
9 changes: 5 additions & 4 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2749,17 +2749,18 @@ namespace FourSlash {
}
}

public verifyRefactorAvailable(negative: boolean, name?: string) {
public verifyRefactorAvailable(negative: boolean, name?: string, subName?: string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

negative [](start = 39, length = 8)

We seem to have an existing pattern, but this is a particularly pointed example of using a predicate with a negative name.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to unify the verify.something and verify.not.something functions

const selection = {
pos: this.currentCaretPosition,
end: this.selectionEnd === -1 ? this.currentCaretPosition : this.selectionEnd
};

let refactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, selection) || [];
if (name) {
refactors = refactors.filter(r => r.name === name);
refactors = refactors.filter(r => r.name === name && (subName === undefined || r.actions.some(a => a.name === subName)));
}
const isAvailable = refactors.length > 0;

if (negative && isAvailable) {
this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected no refactor but found some.`);
}
Expand Down Expand Up @@ -3668,8 +3669,8 @@ namespace FourSlashInterface {
this.state.verifyApplicableRefactorAvailableForRange(this.negative);
}

public refactorAvailable(name?: string) {
this.state.verifyRefactorAvailable(this.negative, name);
public refactorAvailable(name?: string, subName?: string) {
this.state.verifyRefactorAvailable(this.negative, name, subName);
}
}

Expand Down
78 changes: 67 additions & 11 deletions src/services/refactors/extractMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ namespace ts.refactor.extractMethod {
export const FunctionWillNotBeVisibleInTheNewScope = createMessage("Function will not visible in the new scope.");
export const InsufficientSelection = createMessage("Select more than a single identifier.");
export const CannotExtractExportedEntity = createMessage("Cannot extract exported declaration");
export const CannotCombineWritesAndReturns = createMessage("Cannot combine writes and returns");
}

export enum RangeFacts {
Expand Down Expand Up @@ -226,13 +227,22 @@ namespace ts.refactor.extractMethod {
return { targetRange: { range: statements, facts: rangeFacts, declarations } };
}
else {
// We have a single expression (start)
const errors = checkRootNode(start) || checkNode(start);
if (errors) {
return { errors };
}

// If our selection is the expression in an ExrpessionStatement, expand
// the selection to include the enclosing Statement (this stops us
// from trying to care about the return value of the extracted function
// and eliminates double semicolon insertion in certain scenarios)
const range = isStatement(start)
? [start]
: <Expression>start;
: start.parent && start.parent.kind === SyntaxKind.ExpressionStatement
? [start.parent as Statement]
: <Expression>start;

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

Expand Down Expand Up @@ -581,7 +591,7 @@ namespace ts.refactor.extractMethod {
}

export function extractFunctionInScope(
node: Node,
node: Statement | Expression | Block,
scope: Scope,
{ usages: usagesInScope, substitutions }: ScopeUsages,
range: TargetRange,
Expand All @@ -597,7 +607,8 @@ namespace ts.refactor.extractMethod {
functionNameText = getUniqueName(n => props.every(p => p.name !== n));
}
else {
functionNameText = getUniqueName(n => !(scope.locals && scope.locals.has(n)));
const file = scope.getSourceFile();
functionNameText = getUniqueName(n => !file.identifiers.has(n as string));
}
const isJS = isInJavaScriptFile(scope);

Expand Down Expand Up @@ -706,10 +717,24 @@ namespace ts.refactor.extractMethod {
if (returnValueProperty) {
assignments.push(createShorthandPropertyAssignment(returnValueProperty));
}

// propagate writes back
newNodes.push(createStatement(createBinary(createObjectLiteral(assignments), SyntaxKind.EqualsToken, call)));
if (returnValueProperty) {
newNodes.push(createReturn(createIdentifier(returnValueProperty)));
if (assignments.length === 1) {
if (returnValueProperty) {
newNodes.push(createReturn(createIdentifier(returnValueProperty)));
}
else {
newNodes.push(createStatement(createBinary(assignments[0].name, SyntaxKind.EqualsToken, call)));
}
}
else {
// emit e.g.
// { a, b, __return } = newFunction(a, b);
// return __return;
newNodes.push(createStatement(createBinary(createObjectLiteral(assignments), SyntaxKind.EqualsToken, call)));
if (returnValueProperty) {
newNodes.push(createReturn(createIdentifier(returnValueProperty)));
}
}
}
else {
Expand Down Expand Up @@ -751,17 +776,23 @@ namespace ts.refactor.extractMethod {
function transformFunctionBody(body: Node) {
if (isBlock(body) && !writes && substitutions.size === 0) {
// already block, no writes to propagate back, no substitutions - can use node as is
return { body, returnValueProperty: undefined };
return { body: createBlock(body.statements, /*multLine*/ true), returnValueProperty: undefined };
}
let returnValueProperty: string;
const statements = createNodeArray(isBlock(body) ? body.statements.slice(0) : [isStatement(body) ? body : createReturn(<Expression>body)]);
// rewrite body if either there are writes that should be propagated back via return statements or there are substitutions
if (writes || substitutions.size) {
const rewrittenStatements = visitNodes(statements, visitor).slice();
if (writes && !(range.facts & RangeFacts.HasReturn)) {
if (writes && !(range.facts & RangeFacts.HasReturn) && isStatement(body)) {
// add return at the end to propagate writes back in case if control flow falls out of the function body
// it is ok to know that range has at least one return since it we only allow unconditional returns
rewrittenStatements.push(createReturn(createObjectLiteral(getPropertyAssignmentsForWrites(writes))));
const assignments = getPropertyAssignmentsForWrites(writes);
if (assignments.length === 1) {
rewrittenStatements.push(createReturn(assignments[0].name));
}
else {
rewrittenStatements.push(createReturn(createObjectLiteral(assignments)));
}
}
return { body: createBlock(rewrittenStatements, /*multiLine*/ true), returnValueProperty };
}
Expand All @@ -778,7 +809,12 @@ namespace ts.refactor.extractMethod {
}
assignments.push(createPropertyAssignment(returnValueProperty, visitNode((<ReturnStatement>node).expression, visitor)));
}
return createReturn(createObjectLiteral(assignments));
if (assignments.length === 1) {
return createReturn(assignments[0].name as Expression);
}
else {
return createReturn(createObjectLiteral(assignments));
}
}
else {
const substitution = substitutions.get(getNodeId(node).toString());
Expand Down Expand Up @@ -852,7 +888,20 @@ namespace ts.refactor.extractMethod {
const target = isReadonlyArray(targetRange.range) ? createBlock(<Statement[]>targetRange.range) : targetRange.range;
const containingLexicalScopeOfExtraction = isBlockScope(scopes[0], scopes[0].parent) ? scopes[0] : getEnclosingBlockScopeContainer(scopes[0]);

forEachChild(target, collectUsages);
collectUsages(target);

for (let i = 0; i < scopes.length; i++) {
let hasWrite = false;
usagesPerScope[i].usages.forEach(value => {
if (value.usage === Usage.Write) {
hasWrite = true;
return false;
}
});
if (hasWrite && !isArray(targetRange.range) && isExpression(targetRange.range)) {
errorsPerScope[i].push(createDiagnosticForNode(targetRange.range, Messages.CannotCombineWritesAndReturns));
}
}

// If there are any declarations in the extracted block that are used in the same enclosing
// lexical scope, we can't move the extraction "up" as those declarations will become unreachable
Expand Down Expand Up @@ -882,6 +931,13 @@ namespace ts.refactor.extractMethod {
collectUsages(node.operand);
valueUsage = savedValueUsage;
}
else if (isPropertyAccessExpression(node) || isElementAccessExpression(node)) {
const savedValueUsage = valueUsage;
// use 'write' as default usage for values
valueUsage = Usage.Read;
forEachChild(node, collectUsages);
valueUsage = savedValueUsage;
}
else if (isIdentifier(node)) {
if (!node.parent) {
return;
Expand Down
12 changes: 6 additions & 6 deletions tests/baselines/reference/extractMethod/extractMethod1.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ namespace A {
function a() {
let a = 1;

({ a } = newFunction(a));
a = newFunction(a);
}

function newFunction(a: number) {
let y = 5;
let z = x;
a = y;
foo();
return { a };
return a;
}
}
}
Expand All @@ -64,7 +64,7 @@ namespace A {
function a() {
let a = 1;

({ a } = newFunction(a));
a = newFunction(a);
}
}

Expand All @@ -73,7 +73,7 @@ namespace A {
let z = x;
a = y;
foo();
return { a };
return a;
}
}
==SCOPE::file '/a.ts'==
Expand All @@ -85,7 +85,7 @@ namespace A {
function a() {
let a = 1;

({ a } = newFunction(x, a, foo));
a = newFunction(x, a, foo);
}
}
}
Expand All @@ -94,5 +94,5 @@ function newFunction(x: number, a: number, foo: () => void) {
let z = x;
a = y;
foo();
return { a };
return a;
}
12 changes: 6 additions & 6 deletions tests/baselines/reference/extractMethod/extractMethod5.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ namespace A {
function a() {
let a = 1;

({ a } = newFunction(a));
a = newFunction(a);
}

function newFunction(a: number) {
let y = 5;
let z = x;
a = y;
foo();
return { a };
return a;
}
}
}
Expand All @@ -64,7 +64,7 @@ namespace A {
function a() {
let a = 1;

({ a } = newFunction(a));
a = newFunction(a);
}
}

Expand All @@ -73,7 +73,7 @@ namespace A {
let z = x;
a = y;
foo();
return { a };
return a;
}
}
==SCOPE::file '/a.ts'==
Expand All @@ -85,7 +85,7 @@ namespace A {
function a() {
let a = 1;

({ a } = newFunction(x, a));
a = newFunction(x, a);
}
}
}
Expand All @@ -94,5 +94,5 @@ function newFunction(x: number, a: number) {
let z = x;
a = y;
A.foo();
return { a };
return a;
}
24 changes: 24 additions & 0 deletions tests/cases/fourslash/extract-method14.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// <reference path='fourslash.ts' />

// Don't emit type annotations in JavaScript files
// Also tests that single-variable return extractions don't get superfluous destructuring

// @allowNonTsExtensions: true
// @Filename: foo.js
//// function foo() {
//// var i = 10;
//// /*a*/return i++;/*b*/
//// }

goTo.select('a', 'b');
edit.applyRefactor('Extract Method', 'scope_1');
verify.currentFileContentIs(`function foo() {
var i = 10;
var __return: any;
({ i, __return } = newFunction(i));
return __return;
}
function newFunction(i) {
return { i, __return: i++ };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You assign i before i++ happens. Meaning at the call site, i will not actually have the incremented value. In this case, that's fine because you return immediately, but here's a case that breaks:

function foo() {
    var i = 10;
    
    function inner() {
        return i++;  // extract this line into file scope, the console output changes
    }

    inner();
    console.log(i);
} 

foo();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that __return should be computed first.

}
`);
22 changes: 22 additions & 0 deletions tests/cases/fourslash/extract-method15.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// <reference path='fourslash.ts' />

// Extracting an increment expression (not statement) should do the right thing,
// including not generating extra destructuring unless needed

//// function foo() {
//// var i = 10;
//// /*a*/i++/*b*/;
//// }

goTo.select('a', 'b');
edit.applyRefactor('Extract Method', 'scope_1');

verify.currentFileContentIs(`function foo() {
var i = 10;
i = newFunction(i);
}
function newFunction(i: number) {
i++;
return i;
}
`);
10 changes: 10 additions & 0 deletions tests/cases/fourslash/extract-method17.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// <reference path='fourslash.ts' />

//// function foo () {
//// var x = 3;
//// var y = /*start*/x++ + 5/*end*/;
//// }

goTo.select('start', 'end')
verify.refactorAvailable('Extract Method', 'scope_0');
verify.not.refactorAvailable('Extract Method', 'scope_1');
21 changes: 21 additions & 0 deletions tests/cases/fourslash/extract-method18.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// <reference path='fourslash.ts' />

// Don't try to propagate property accessed variables back,
// or emit spurious returns when the value is clearly ignored

//// function fn() {
//// const x = { m: 1 };
//// /*a*/x.m = 3/*b*/;
//// }

goTo.select('a', 'b')
verify.refactorAvailable('Extract Method');
edit.applyRefactor('Extract Method', "scope_1");
verify.currentFileContentIs(`function fn() {
const x = { m: 1 };
newFunction(x);
}
function newFunction(x: { m: number; }) {
x.m = 3;
}
`);
2 changes: 1 addition & 1 deletion tests/cases/fourslash/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ declare namespace FourSlashInterface {
codeFixDiagnosticsAvailableAtMarkers(markerNames: string[], diagnosticCode?: number): void;
applicableRefactorAvailableForRange(): void;

refactorAvailable(name?: string);
refactorAvailable(name?: string, subName?: string);
}
class verify extends verifyNegatable {
assertHasRanges(ranges: Range[]): void;
Expand Down