-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
Extract Method #17625
Conversation
Extracting
yields
It looks like a semicolon is required between |
Does extracting the LHS of an assignment ever make sense? |
Channelling Vladimir Reshetnikov: extracting
yields
|
Should it be impossible to extract one or more empty statements? |
@@ -18,6 +18,13 @@ | |||
"problemMatcher": [ | |||
"$tsc" | |||
] | |||
}, | |||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this do?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
src/compiler/checker.ts
Outdated
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/compiler/utilities.ts
Outdated
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/compiler/utilities.ts
Outdated
return isStatementKindButNotDeclarationKind(kind) | ||
|| isDeclarationStatementKind(kind) | ||
|| node.kind === SyntaxKind.Block; | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (edits[j].span.start >= edits[i].span.start) { | ||
edits[j].span.start += editDelta; | ||
} | ||
} |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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)
src/harness/fourslash.ts
Outdated
const isAvailable = refactors.length > 0; | ||
|
||
if (negative && isAvailable) { | ||
this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected no refactor but found some.`); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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++; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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; | ||
}; |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; | ||
} | ||
} |
There was a problem hiding this comment.
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))); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)) && |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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)); | ||
} |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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*/[], |
There was a problem hiding this comment.
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))] |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
Did we end up doing anything about chained assignments after our discussion on the unsquashed PR? |
src/compiler/checker.ts
Outdated
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); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
src/compiler/utilities.ts
Outdated
|| node.kind === SyntaxKind.Block; | ||
} | ||
|
||
function isBlockStatement(node: Node): node is Block { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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.
Continuation of #16960