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? |
| "$tsc" | ||
| ] | ||
| }, | ||
| { |
There was a problem hiding this comment.
Allows debugging single tests from VS Code by pressing F5 in the testcase's editor window
There was a problem hiding this comment.
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.
Does name as __String perform the same escaping as escapeLeadingUnderscores or was that unnecessary?
There was a problem hiding this comment.
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.
src/compiler/utilities.ts
Outdated
| 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.
src/compiler/utilities.ts
Outdated
| return isStatementKindButNotDeclarationKind(kind) | ||
| || isDeclarationStatementKind(kind) | ||
| || node.kind === SyntaxKind.Block; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
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.
| if (edits[j].span.start >= edits[i].span.start) { | ||
| edits[j].span.start += editDelta; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
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.
some [](start = 115, length = 4)
Would it be helpful to list the refactors we found?
| * 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.
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.
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.
(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.
get [](start = 27, length = 3)
has might be more explicit.
| name: `scope_${i}` | ||
| }); | ||
| } | ||
| i++; |
There was a problem hiding this comment.
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.
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.
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.
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.
/scope_(\d+)/ [](start = 33, length = 13)
Is this a substring match? If so, would ^ and $ help?
| } | { | ||
| readonly targetRange: TargetRange; | ||
| readonly errors?: never; | ||
| }; |
| return { targetRange: { range: statements, facts: rangeFacts, declarations } }; | ||
| } | ||
| else { | ||
| // We have a single expression (start) |
There was a problem hiding this comment.
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.
| return { errors }; | ||
| } | ||
|
|
||
| // If our selection is the expression in an ExrpessionStatement, expand |
There was a problem hiding this comment.
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.
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.
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.
if [](start = 40, length = 2)
"whether"
| } | ||
| current = current.parent; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
) [](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.
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?
| // 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.
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.
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.
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.
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.
__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.
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.
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.
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.
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.
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.
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.
Would it be more conventional to put static before async?
There was a problem hiding this comment.
Conventional -> correct 😅
| modifiers.push(createToken(SyntaxKind.StaticKeyword)); | ||
| } | ||
| newFunction = createMethod( | ||
| /*decorators*/ undefined, |
There was a problem hiding this comment.
/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.
/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.
createKeywordTypeNode(SyntaxKind.AnyKeyword) [](start = 68, length = 44)
Why not use returnType?
| } | ||
|
|
||
| function generateReturnValueProperty() { | ||
| return "__return"; |
There was a problem hiding this comment.
"__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.
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.
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.
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.
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.
amcasey
left a comment
There was a problem hiding this comment.
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.
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.
| */ | ||
| /* @internal */ getAllPossiblePropertiesOfType(type: Type): Symbol[]; | ||
|
|
||
| /* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags): Symbol | undefined; |
There was a problem hiding this comment.
See comment on the implementation.
| || node.kind === SyntaxKind.Block; | ||
| } | ||
|
|
||
| function isBlockStatement(node: Node): node is Block { |
There was a problem hiding this comment.
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.
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.
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.
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