-
Notifications
You must be signed in to change notification settings - Fork 13.1k
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
Extract method #16960
Conversation
|
Tests should be fixed by ivogabe/gulp-typescript#522 |
|
🔔 |
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.
Not all selections will produce the same set of extractions. For example, if we refer to a property of this in the selection body, then we won't be able to extract to the enclosing namespace or file
You could add an option to extract to namespace where the this object becomes a normal function parameter, rather than this, provided no private or protected members are in use in the extracted code. I've done this by hand frequently in the past (extracting internal code into an external helper) - it may be worth adding. Actually, even private member access can be transformed to extracted-function-parameters provided they don't have unexposed types. Maybe an enhancement for the future.
| resolveNameAtLocation(location: Node, name: string, meaning: SymbolFlags): Symbol | undefined { | ||
| location = getParseTreeNode(location); | ||
| return resolveName(location, name, meaning, /*nameNotFoundMessage*/ undefined, name); | ||
| resolveName(name, location, meaning) { |
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.
Is this the same as resolveNameAtLocation, but with a changed argument order and no looking up of parse tree nodes (I can only assume so it works on synthesized nodes with parent pointers setup)? Can we not just remove the looking up of parse tree nodes in resolveNameAtLocation if that's the case? Oh, nameArg is also undefined. Can that not just be added as an optional parameter to this existing function?
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.
@rbuckton would know authoritatively, but as I understand it, we check isParseTreeNode on public API requests because the checker does not work well with synthesized nodes, since we don't perform binding on synthesized nodes as part of transforms (for performance reasons?) and we may inspect the literal text as part of some checker operations. That is, the checker API is only supposed to work with trees parsed and bound from an actual source 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.
@aozgaa is correct. If the checker receives a node that did not originate from a parse tree, we attempt to find the parse tree node from which it was created. Passing in a synthesized node to the checker without a parse-tree check will likely result in compiler crashes.
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 said, resolveNameAtLocation (nee. resolveName) is marked /* @internal */ and isn't used in transforms. The guard is mostly present in case we decide to use it at some point down the road.
| if (isToken(child) && child.kind !== SyntaxKind.JsxText) { | ||
| // if child node is a token, it does not impact indentation, proceed it using parent indentation scope rules | ||
| const tokenInfo = formattingScanner.readTokenInfo(child); | ||
| Debug.assert(tokenInfo.token.end === child.end, "Token end is child end"); |
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.
Was this assertion incorrect? This PR didn't change any parsing related things, AFAIK?
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.
Finally tracked this down - the problem is we were creating a synthetic type reference node with text like { x: string } when the type was anonymous, which was confusing the scanner because it expects a type reference node to point to an identifier or qualified name, so the formatting scanner and syntax tree disagreed about whether to expect a token. Fixed in extractMethod#485 and restored the assert
| const nullTransformationContext: TransformationContext = { | ||
| enableEmitNotification: noop, | ||
| enableSubstitution: noop, | ||
| endLexicalEnvironment: () => 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.
Isn't this just noop?
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.
Sort of. noop returns void, but endLexicalEnvironment expects a Statement[] | undefined return value. Switched to noop and added a type assertion
| case SyntaxKind.FunctionDeclaration: | ||
| return `function ${s.name.getText()}`; | ||
| case SyntaxKind.ArrowFunction: | ||
| return "arrow function"; |
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.
These won't be localized.
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 think Roslyn decided not to localize syntax kind names, but their use case is slightly different.
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.
Half these are actual keywords. Do we actually expect the loc team to change e.g. "method" to correct corresponding word in the target language?
|
Does it work with JSX? Can I select some JSX markup and extract it to stateless component? |
src/compiler/types.ts
Outdated
| */ | ||
| /* @internal */ getAllPossiblePropertiesOfType(type: Type): Symbol[]; | ||
|
|
||
| /* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags): Symbol; |
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 happened to | undefined? Does it throw on failure?
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 compiler doesn't have strictNullChecks on. Added | undefined to be explicit.
src/services/textChanges.ts
Outdated
| readonly useIndentationFromFile?: boolean; | ||
| readonly node?: Node; | ||
| readonly options?: ChangeNodeOptions; | ||
| readonly node?: Node | 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.
I'm not sure I understand the type. It's either undefined, one Node, zero Nodes, a singleton list of Nodes, or multiple Nodes?
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.
Changed this to be more explicit, but we actually do allocate a fair number of these and I think it's worth avoiding the array allocation in the (much more common) 1-element case.
src/services/textChanges.ts
Outdated
| } | ||
|
|
||
| public static fromRefactorContext(context: RefactorContext) { | ||
| return new ChangeTracker(context.newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed, context.rulesProvider); |
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 appears to be the third occurrence. Consider extracting context.newLineKind.
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.
👍 but only found two, is the third in another 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.
Yep, separate file.
src/services/textChanges.ts
Outdated
|
|
||
| public replaceNodes(sourceFile: SourceFile, oldNodes: Node | Node[], newNodes: Node | Node[], options: ChangeNodeOptions & ChangeNodesOptions = {}) { | ||
| const startPosition = getAdjustedStartPosition(sourceFile, isArray(oldNodes) ? oldNodes[0] : oldNodes, options, Position.Start); | ||
| const endPosition = getAdjustedEndPosition(sourceFile, isArray(oldNodes) ? lastOrUndefined(oldNodes) : oldNodes, options); |
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.
From context, I assume that lastOrUndefined never returns 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's like the .NET version (actual signature is export function lastOrUndefined<T>(array: T[]): T | 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.
lastOrUndefined(ar) returns undefined if either the ar is undefined or ar is empty. We also have a firstOrUndefined(ar) that does the same thing except it reads the first element.
Generally, lastOrUndefined is just a shortcut for ar[ar.length - 1].
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.
To clarify, this particular call to lastOrUndefined never returns undefined? If that's not the case, is getAdjustedEndPosition going to throw?
src/services/textChanges.ts
Outdated
| } | ||
| // strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line | ||
| // however keep indentation if it is was forced | ||
| text = posStartsLine || change.options.indentation !== undefined ? text : text.replace(/^\s+/, ""); |
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 seems like an expensive way to TrimStart. There isn't a helper?
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.
Also, I'm routinely confused by JS's short-circuiting operators, but won't text be a number if posStartsLine is defined?
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 have a leading trim function yet; since we'd theoretically have to scan for all the Unicode whitespace I'm not sure it can be done more cheaply than that RegEx.
Added parens for clarity (the || expression is the first operand to ?)
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 looks like there is a string.prototype.trim. Is there a reason to keep the whitespace at the end of the string?
src/services/textChanges.ts
Outdated
| } | ||
|
|
||
| private getFormattedTextOfNode(node: Node, sourceFile: SourceFile, pos: number, useIndentationFromFile: boolean, options: ChangeNodeOptions) { | ||
| const nonFormattedText = getNonformattedText(node, sourceFile, this.newLine); |
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.
"nonFormatted" but "Nonformatted"?
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.
👍
| ? change.options.indentation | ||
| : change.useIndentationFromFile | ||
| ? formatting.SmartIndenter.getIndentation(change.range.pos, sourceFile, formatOptions, posStartsLine || (change.options.prefix === this.newLineCharacter)) | ||
| options.indentation !== 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.
The first two lines are just options.indentation ||, aren't they?
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.
options.identation could be 0, which is falsy
src/services/textChanges.ts
Outdated
| } | ||
|
|
||
| export interface ChangeNodesOptions { | ||
| nodesSeparator?: 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.
Is this also applicable to replaceNodeRange?
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.
More accurate but impossibly-verbose names:
ChangeNodesOptions -> OptionsWhenInsertingMoreThanOneNewNode
ChangeNodeOptions -> OptionsWhenInsertingOneNode
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 believe you've cleaned this up offline.
|
It seems to be impossible to extract a single identifier into a method. Is that intentional? e.g. I might want to wrap access to a variable in a function so that I can bracket it with EH or logging. |
| return __return; | ||
| } | ||
| function newFunction(i) { | ||
| return { i, __return: 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.
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();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 agree that __return should be computed first.
| 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 you eliminate the need to do this by walking the list backwards?
src/harness/fourslash.ts
Outdated
| if (negative && isAvailable) { | ||
| this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected no refactor but found some.`); | ||
| } | ||
| if (!negative && !isAvailable) { |
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 = 12, length = 2)
else if
src/harness/fourslash.ts
Outdated
| } | ||
| } | ||
|
|
||
| public verifyRefactorAvailable(negative: boolean, name?: string, subName?: 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.
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.
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 to unify the verify.something and verify.not.something functions
src/harness/fourslash.ts
Outdated
| } | ||
|
|
||
| public applyRefactor(refactorName: string, actionName: string) { | ||
| const range = { pos: this.currentCaretPosition, end: this.selectionEnd === -1 ? this.currentCaretPosition : this.selectionEnd }; |
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.
{ pos: this.currentCaretPosition, end: this.selectionEnd === -1 ? this.currentCaretPosition : this.selectionEnd } [](start = 26, length = 113)
Duplicated from above?
src/services/textChanges.ts
Outdated
| return new ChangeTracker(getNewlineKind(context), context.rulesProvider); | ||
| } | ||
|
|
||
| public static fromRefactorContext(context: RefactorContext) { |
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.
fromRefactorContext [](start = 22, length = 19)
Can this be merged with fromCodeFixContext?
src/services/textChanges.ts
Outdated
| return this; | ||
| } | ||
|
|
||
| private replaceWithMutiple(sourceFile: SourceFile, startPosition: number, endPosition: number, newNodes: ReadonlyArray<Node>, options: ChangeMultipleNodesOptions): this { |
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.
replaceWithMutiple [](start = 16, length = 18)
Typo
| const parts = change.nodes.map(n => this.getFormattedTextOfNode(n, sourceFile, pos, options)); | ||
| text = parts.join(change.options.nodeSeparator); | ||
| } | ||
| else { |
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.
else { [](start = 12, length = 6)
Would it be worthwhile to assert the kind so that we get some kind of warning if a new one is added?
src/services/textChanges.ts
Outdated
| text = this.getFormattedTextOfNode(change.node, sourceFile, pos, options); | ||
| } | ||
| // strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line | ||
| // however keep indentation if it is was forced |
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.
Is this comment still accurate?
| ? formatting.SmartIndenter.getIndentation(change.range.pos, sourceFile, formatOptions, posStartsLine || (change.options.prefix === this.newLineCharacter)) | ||
| options.indentation !== undefined | ||
| ? options.indentation | ||
| : (options.useIndentationFromFile !== false) |
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.
(options.useIndentationFromFile !== false) [](start = 22, length = 42)
!options.useIndentationFromFile?
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 want true and undefined to behave the same here, hence the odd conditional
| ({ a } = newFunction(a)); | ||
| } | ||
|
|
||
| function newFunction(a: 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.
any [](start = 32, length = 3)
number?
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.
Something's going weird with the unit test harness setup - this gets : number when I do it in the editor
| export interface I { x: number }; | ||
| class C { | ||
| a() { | ||
| let z = 1; |
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.
let z = 1; [](start = 11, length = 11)
Did you intend to consume z or is this a way to confirm that it's ignored?
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 didn't write it, but I'd go with the latter
|
Type is referenced from context where it's not visible: minestarks@d1fa06e#diff-92a31e9489e4badbd019ff88953fe79f |
Extract
|
|
I suspect @minestarks or I may already have reported this, but if you extract |
|
Fresh sheet of paper at #17625 |
Implements the "extract function" refactor
Given a user selection, this produces a set of "extractions" the user can select from. For example, with an expression in a class, we can refactor into a peer method, a function in an enclosing namespace, or a function in the same file (these are referred to as "scopes" in the code):
Not all selections will produce the same set of extractions. For example, if we refer to a property of
thisin the selection body, then we won't be able to extract to the enclosing namespace or file:Still writing some tests so expect a few commits with bugfixes to come in, but it's pushing 2,000 lines of implementation already so jump in
Most code courtesy @vladima so I may not be able to perfectly answer all questions