Skip to content

Extract Method #17625

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

Merged
merged 9 commits into from
Aug 11, 2017
Merged

Extract Method #17625

merged 9 commits into from
Aug 11, 2017

Conversation

RyanCavanaugh
Copy link
Member

Continuation of #16960

@amcasey
Copy link
Member

amcasey commented Aug 7, 2017

Extracting [0] from

var q = [0]
q[0]++

yields

var q = newFunction()q[0]++

function newFunction() {
    return [0];
}

It looks like a semicolon is required between () and q.

@amcasey
Copy link
Member

amcasey commented Aug 7, 2017

Does extracting the LHS of an assignment ever make sense?

@amcasey
Copy link
Member

amcasey commented Aug 7, 2017

Channelling Vladimir Reshetnikov: extracting []

function F() {
    return[]
}

yields

function F() {
    returnnewFunction()
    function newFunction() {
        return [];
    }
}

@amcasey
Copy link
Member

amcasey commented Aug 7, 2017

Should it be impossible to extract one or more empty statements?

@@ -18,6 +18,13 @@
"problemMatcher": [
"$tsc"
]
},
{
Copy link
Member

Choose a reason for hiding this comment

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

What does this do?

Copy link
Member Author

Choose a reason for hiding this comment

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

Allows debugging single tests from VS Code by pressing F5 in the testcase's editor window

Copy link
Member

Choose a reason for hiding this comment

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

That sounds great. How does it do that?

location = getParseTreeNode(location);
return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, escapeLeadingUnderscores(name));
resolveName(name, location, meaning) {
return resolveName(location, name as __String, meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined);
Copy link
Member

Choose a reason for hiding this comment

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

Does name as __String perform the same escaping as escapeLeadingUnderscores or was that unnecessary?

Copy link
Member

Choose a reason for hiding this comment

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

It does not. It is probably needed, as without it, submitting the string __proto__ will fail to find the correct symbol (since we will attempt to find an internal symbol named __proto__, rather than looking up ___proto__, which is where we keep the real user symbol for that name.). The caveat to escaping the input is that it becomes impossible to look up internal symbols like __call - but external users shouldn't be doing that anyway, right? Anyways, if you don't call escapeLeadingUnderscores, the resolveName signature should use __String instead of string (rendering the cast unneeded), to punt the escape required/not required issue to the API consumer, rather than doing something unsafe here.

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

@@ -693,7 +693,7 @@ namespace ts {
// At this point, node is either a qualified name or an identifier
Debug.assert(node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.QualifiedName || node.kind === SyntaxKind.PropertyAccessExpression,
"'node' was expected to be a qualified name, identifier or property access in 'isPartOfTypeNode'.");
// falls through
// falls through
Copy link
Member

Choose a reason for hiding this comment

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

Intentional?

Copy link
Member

Choose a reason for hiding this comment

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

Below too.


In reply to: 132297135 [](ancestors = 132297135)

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

return isStatementKindButNotDeclarationKind(kind)
|| isDeclarationStatementKind(kind)
|| node.kind === SyntaxKind.Block;
}
Copy link
Member

Choose a reason for hiding this comment

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

What about just having the only caller check node.kind === SyntaxKind.Block || isStatement(node)? This function is kind of hard to explain as an independent entity.

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

assert.isTrue(firstEditEnd <= edits[i + 1].span.start);
}
// Copy this so we don't ruin someone else's copy
edits = JSON.parse(JSON.stringify(edits));
Copy link
Member

@amcasey amcasey Aug 9, 2017

Choose a reason for hiding this comment

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

JSON.parse(JSON.stringify(edits)); [](start = 20, length = 34)

This seems like a particularly expensive way to accomplish the copy. #WontFix

Copy link
Member

Choose a reason for hiding this comment

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

Never mind - didn't see that this was test code.


In reply to: 132299991 [](ancestors = 132299991)

if (edits[j].span.start >= edits[i].span.start) {
edits[j].span.start += editDelta;
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Could we avoid this work by sorting the edits in descending order of start position? (That would also make it easy to restore the no-overlap assertion.)

Copy link
Member

Choose a reason for hiding this comment

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

Given that this is test code, I don't particularly care about the extra work, but having the assert seems worthwhile.


In reply to: 132300325 [](ancestors = 132300325)

const isAvailable = refactors.length > 0;

if (negative && isAvailable) {
this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected no refactor but found some.`);
Copy link
Member

Choose a reason for hiding this comment

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

some [](start = 115, length = 4)

Would it be helpful to list the refactors we found?

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

* Each returned ExtractResultForScope corresponds to a possible target scope and is either a set of changes
* or an error explaining why we can't extract into that scope.
*/
export function extractRange(targetRange: TargetRange, context: RefactorContext, requestedChangesIndex: number = undefined): ReadonlyArray<ExtractResultForScope> | undefined {
Copy link
Member

Choose a reason for hiding this comment

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

extractRange [](start = 20, length = 12)

Personally, if would find a name like "getExtractResults" or "getPossibleExtractions" clearer. In particular, it's very hard to tell at the call site what this method is expected to return.

continue;
}

// Don't issue refactorings with duplicated names
Copy link
Member

Choose a reason for hiding this comment

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

Don't issue refactorings with duplicated names [](start = 15, length = 46)

Is the order of extractions guaranteed? Which duplicate is expected to "win"?

Copy link
Member Author

Choose a reason for hiding this comment

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

(leave comment indicating that the inner scope always 'wins' due to the list being order "inner scopes first")


// Don't issue refactorings with duplicated names
const description = formatStringFromArgs(Diagnostics.Extract_function_into_0.message, [extr.scopeDescription]);
if (!usedNames.get(description)) {
Copy link
Member

Choose a reason for hiding this comment

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

get [](start = 27, length = 3)

has might be more explicit.

name: `scope_${i}`
});
}
i++;
Copy link
Member

Choose a reason for hiding this comment

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

i++; [](start = 12, length = 4)

We increment whether or not we add an entry to the list. Is that intentional? If not, the length of actions is probably sufficient.

Copy link
Member

Choose a reason for hiding this comment

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

Looks like it's important to leave gaps in the sequence. Consider adding a comment to that effect.


In reply to: 132311523 [](ancestors = 132311523)

if (extractions === undefined) {
// Scope is no longer valid from when the user issued the refactor
// return undefined;
return undefined;
Copy link
Member

Choose a reason for hiding this comment

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

This should be very rare, right? Cases like the file refreshing in the background?


const parsedIndexMatch = /scope_(\d+)/.exec(actionName);
if (!parsedIndexMatch) {
throw new Error("Expected to match the regexp");
Copy link
Member

Choose a reason for hiding this comment

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

throw new Error("Expected to match the regexp"); [](start = 12, length = 48)

Can this occur other than through programmer error?

const rangeToExtract = getRangeToExtract(context.file, { start: context.startPosition, length });
const targetRange: TargetRange = rangeToExtract.targetRange;

const parsedIndexMatch = /scope_(\d+)/.exec(actionName);
Copy link
Member

Choose a reason for hiding this comment

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

/scope_(\d+)/ [](start = 33, length = 13)

Is this a substring match? If so, would ^ and $ help?

} | {
readonly targetRange: TargetRange;
readonly errors?: never;
};
Copy link
Member

Choose a reason for hiding this comment

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

Cute. :)

return { targetRange: { range: statements, facts: rangeFacts, declarations } };
}
else {
// We have a single expression (start)
Copy link
Member

Choose a reason for hiding this comment

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

expression [](start = 32, length = 10)

Why does it have to be an expression? Couldn't it be (e.g.) a statement?

Copy link
Member

Choose a reason for hiding this comment

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

We check isStatement below...


In reply to: 132315243 [](ancestors = 132315243)

Copy link
Member Author

Choose a reason for hiding this comment

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

"single node"

return { errors };
}

// If our selection is the expression in an ExrpessionStatement, expand
Copy link
Member

Choose a reason for hiding this comment

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

ExrpessionStatement [](start = 56, length = 19)

Typo

? [start]
: start.parent && start.parent.kind === SyntaxKind.ExpressionStatement
? [start.parent as Statement]
: <Expression>start;
Copy link
Member

Choose a reason for hiding this comment

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

start [](start = 22, length = 17)

Is this cast guaranteed to succeed? There are no other types that start could have?

Copy link
Member Author

Choose a reason for hiding this comment

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

Match assertion styles

return [createDiagnosticForNode(nodeToCheck, Messages.CannotExtractAmbientBlock)];
}

// If we're in a class, see if we're in a static region (static property initializer, static method, class constructor parameter default) or not
Copy link
Member

Choose a reason for hiding this comment

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

if [](start = 40, length = 2)

"whether"

}
current = current.parent;
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Consider pulling this out into its own method.

if (isDeclaration(node)) {
const declaringNode = (node.kind === SyntaxKind.VariableDeclaration) ? node.parent.parent : node;
if (hasModifier(declaringNode, ModifierFlags.Export)) {
(errors || (errors = []).push(createDiagnosticForNode(node, Messages.CannotExtractExportedEntity)));
Copy link
Member

Choose a reason for hiding this comment

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

) [](start = 47, length = 1)

Suppose to be ))?

}

function isValidExtractionTarget(node: Node) {
// Note that we don't use isFunctionLike because we don't want to put the extracted closure *inside* a method
Copy link
Member

Choose a reason for hiding this comment

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

we don't want to put the extracted closure inside a method [](start = 57, length = 60)

I'm sure I'm missing something obvious, but why not?

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 stranding

// A function parameter's initializer is actually in the outer scope, not the function declaration
if (current && current.parent && current.parent.kind === SyntaxKind.Parameter) {
// Skip all the way to the outer scope of the function that declared this parameter
current = current.parent.parent.parent;
Copy link
Member

Choose a reason for hiding this comment

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

current.parent.parent.parent [](start = 26, length = 28)

This makes me a bit nervous. Consider asserting that the result is of an appropriate kind.

Copy link
Member

Choose a reason for hiding this comment

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

Or use findAncestor - I believe it should assert the kind, once found.

}
else {
const file = scope.getSourceFile();
functionNameText = getUniqueName(n => !file.identifiers.has(n as string));
Copy link
Member

Choose a reason for hiding this comment

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

as string [](start = 74, length = 9)

I didn't think direct conversion between __String and string was safe. Is this a special case?

Copy link
Member

@weswigham weswigham Aug 10, 2017

Choose a reason for hiding this comment

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

This is fine just because getUniqueName always returns a string which begins with newFunction, which would never need to be escaped.... that's a nonlocal assumption to make, though, and would need to be documented in a comment, IMO. But all use sites for getUniqueName require a string, so should really just operate on strings instead of escaped strings.

}

function getUniqueName(isNameOkay: (name: __String) => boolean) {
let functionNameText = "newFunction" as __String;
Copy link
Member

Choose a reason for hiding this comment

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

__String [](start = 48, length = 8)

It looks like the only consumers expect string.

}
}

function isModuleBlock(n: Node): n is ModuleBlock {
Copy link
Member

Choose a reason for hiding this comment

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

I would have guessed there was a family of such type guards in a shared location.

* such as `import x from 'y'` -- the 'y' is a StringLiteral but is *not* an expression
* in the sense of something that you could extract on
*/
function isLegalExpressionExtraction(node: Node): boolean {
Copy link
Member

Choose a reason for hiding this comment

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

isLegalExpressionExtraction [](start = 13, length = 27)

"isExtractableExpression"?

* such as `import x from 'y'` -- the 'y' is a StringLiteral but is *not* an expression
* in the sense of something that you could extract on
*/
function isLegalExpressionExtraction(node: Node): boolean {
Copy link
Member

Choose a reason for hiding this comment

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

boolean [](start = 54, length = 7)

node is Expression?

switch (node.kind) {
case SyntaxKind.StringLiteral:
return node.parent.kind !== SyntaxKind.ImportDeclaration &&
node.parent.kind !== SyntaxKind.ImportSpecifier;
Copy link
Member

Choose a reason for hiding this comment

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

The parent should probably also not be an export.

}

function spanContainsNode(span: TextSpan, node: Node, file: SourceFile): boolean {
return textSpanContainsPosition(span, node.getStart(file)) &&
Copy link
Member

Choose a reason for hiding this comment

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

textSpanContainsPosition(span, node.getStart(file)) [](start = 15, length = 52)

Why not just compare the start positions? (Both for symmetry and to avoid the unnecessary comparison between the node start and the span end.)

}

function getParentNodeInSpan(node: Node, file: SourceFile, span: TextSpan): Node {
while (node) {
Copy link
Member

Choose a reason for hiding this comment

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

node [](start = 15, length = 4)

If we don't expect the argument to be undefined, the loop exit condition is probably redundant.

}
if (range.facts & RangeFacts.InStaticRegion) {
modifiers.push(createToken(SyntaxKind.StaticKeyword));
}
Copy link
Member

Choose a reason for hiding this comment

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

Would it be more conventional to put static before async?

Copy link
Member Author

Choose a reason for hiding this comment

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

Conventional -> correct 😅

modifiers.push(createToken(SyntaxKind.StaticKeyword));
}
newFunction = createMethod(
/*decorators*/ undefined,
Copy link
Member

Choose a reason for hiding this comment

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

/decorators/ undefined [](start = 16, length = 24)

We might eventually want to propagate decorators (e.g. @readonly on a parameter.

range.facts & RangeFacts.IsGenerator ? createToken(SyntaxKind.AsteriskToken) : undefined,
functionName,
/*questionToken*/ undefined,
/*typeParameters*/[],
Copy link
Member

Choose a reason for hiding this comment

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

/typeParameters/[] [](start = 16, length = 20)

Don't we need to have type parameters if they're consumed by the extracted code and not in scope in the new method?

// has both writes and return, need to create variable declaration to hold return value;
newNodes.push(createVariableStatement(
/*modifiers*/ undefined,
[createVariableDeclaration(returnValueProperty, createKeywordTypeNode(SyntaxKind.AnyKeyword))]
Copy link
Member

Choose a reason for hiding this comment

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

createKeywordTypeNode(SyntaxKind.AnyKeyword) [](start = 68, length = 44)

Why not use returnType?

}

function generateReturnValueProperty() {
return "__return";
Copy link
Member

Choose a reason for hiding this comment

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

"__return" [](start = 19, length = 10)

Does this have to be made unique?

if (range.facts & RangeFacts.HasReturn) {
newNodes.push(createReturn(call));
}
else if (isArray(range.range)) {
Copy link
Member

Choose a reason for hiding this comment

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

isArray(range.range) [](start = 21, length = 20)

Consider extracting a local for this. It has the opposite sense, but "isExpression" might be the most readable.

const rewrittenStatements = visitNodes(statements, visitor).slice();
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
Copy link
Member

Choose a reason for hiding this comment

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

This sentence has some extra words in it.

return n.kind === SyntaxKind.ModuleBlock;
}

function isReadonlyArray(v: any): v is ReadonlyArray<any> {
Copy link
Member

Choose a reason for hiding this comment

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

isReadonlyArray [](start = 13, length = 15)

I suspect this could replace some of the isArray calls above.

}

if (isAssignmentExpression(node)) {
const savedValueUsage = valueUsage;
Copy link
Member

Choose a reason for hiding this comment

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

valueUsage [](start = 40, length = 10)

Personally, I would just make valueUsage a parameter of collectUsages. That would eliminate both the side effects and the saving/restoration.

Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

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

I'm out of steam for today but I have no fundamental objections to checking this in as-is.

@amcasey
Copy link
Member

amcasey commented Aug 10, 2017

Did we end up doing anything about chained assignments after our discussion on the unsquashed PR?

location = getParseTreeNode(location);
return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, escapeLeadingUnderscores(name));
resolveName(name, location, meaning) {
return resolveName(location, name as __String, meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined);
Copy link
Member

Choose a reason for hiding this comment

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

It does not. It is probably needed, as without it, submitting the string __proto__ will fail to find the correct symbol (since we will attempt to find an internal symbol named __proto__, rather than looking up ___proto__, which is where we keep the real user symbol for that name.). The caveat to escaping the input is that it becomes impossible to look up internal symbols like __call - but external users shouldn't be doing that anyway, right? Anyways, if you don't call escapeLeadingUnderscores, the resolveName signature should use __String instead of string (rendering the cast unneeded), to punt the escape required/not required issue to the API consumer, rather than doing something unsafe here.

@@ -2606,9 +2606,8 @@ namespace ts {
* Does not include properties of primitive types.
*/
/* @internal */ getAllPossiblePropertiesOfType(type: Type): Symbol[];

/* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags): Symbol | undefined;
Copy link
Member

Choose a reason for hiding this comment

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

See comment on the implementation.

|| node.kind === SyntaxKind.Block;
}

function isBlockStatement(node: Node): node is Block {
Copy link
Member

Choose a reason for hiding this comment

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

This name seems misleading - it checks if the node is a block, and that block isn't in a try or a catch (but not finally?), and that it's not a function block? I think the simpler check for use with isStatement is node.kind === SyntaxKind.Block && isBlockLike(node.parent). Since a block is only a block statement when it occurs..... directly inside a block. I don't think we have an isBlockLike written yet, but we do have the BlockLike union type already - which if I'm not mistaken, covers a few more cases than here.

Copy link
Member Author

Choose a reason for hiding this comment

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

I missed the finally case; your suggested implementation sounds much better.

The name is correct (or at least the best one I can come up with?). A block statement is a block that occurs in a grammatical context where a statement is allowed.

// A function parameter's initializer is actually in the outer scope, not the function declaration
if (current && current.parent && current.parent.kind === SyntaxKind.Parameter) {
// Skip all the way to the outer scope of the function that declared this parameter
current = current.parent.parent.parent;
Copy link
Member

Choose a reason for hiding this comment

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

Or use findAncestor - I believe it should assert the kind, once found.

}
else {
const file = scope.getSourceFile();
functionNameText = getUniqueName(n => !file.identifiers.has(n as string));
Copy link
Member

@weswigham weswigham Aug 10, 2017

Choose a reason for hiding this comment

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

This is fine just because getUniqueName always returns a string which begins with newFunction, which would never need to be escaped.... that's a nonlocal assumption to make, though, and would need to be documented in a comment, IMO. But all use sites for getUniqueName require a string, so should really just operate on strings instead of escaped strings.

@RyanCavanaugh RyanCavanaugh merged commit c110ecb into microsoft:master Aug 11, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants