-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
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.
src/compiler/checker.ts
Outdated
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.
@@ -706,7 +706,6 @@ namespace ts.formatting { | |||
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
@@ -2645,9 +2645,8 @@ namespace ts { | |||
* Does not include properties of primitive types. | |||
*/ | |||
/* @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
export type ChangeNodeOptions = ConfigurableStartEnd & InsertNodeOptions; | ||
|
||
interface Change { | ||
readonly sourceFile: SourceFile; | ||
readonly range: TextRange; | ||
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 Node
s, a singleton list of Node
s, or multiple Node
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.
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
@@ -161,6 +165,10 @@ namespace ts.textChanges { | |||
return new ChangeTracker(context.newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed, context.rulesProvider); | |||
} | |||
|
|||
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
@@ -241,6 +249,13 @@ namespace ts.textChanges { | |||
return this; | |||
} | |||
|
|||
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
@@ -83,14 +83,18 @@ namespace ts.textChanges { | |||
delta?: number; | |||
} | |||
|
|||
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
@@ -2736,6 +2749,26 @@ namespace FourSlash { | |||
} | |||
} | |||
|
|||
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
@@ -2752,6 +2785,20 @@ namespace FourSlash { | |||
} | |||
} | |||
|
|||
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
this
in 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