- Notifications
You must be signed in to change notification settings - Fork12.9k
Add refactor convertToOptionalChainExpression#39135
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Add refactor convertToOptionalChainExpression#39135
Uh oh!
There was an error while loading.Please reload this page.
Conversation
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.
Still need to think through the main binary expression loop ingetInfo
, here are the comments I've got so far.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
tests/cases/fourslash/refactorConvertToOptionalChainExpressionForTriggerReason1.ts OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
tests/cases/fourslash/refactorConvertToOptionalChainExpressionForTriggerReason1.ts OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
tests/cases/fourslash/refactorConvertToOptionalChainExpression12.ts OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>
Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>
…ForTriggerReason3.tsCo-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>
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.
Most of your test cases are expression statements, but expression statements that don’t immediately contain call expressions are somewhat rare. If the idea is to automatically find a suitable expression node from a zero-width span, I think you could try just walking up ancestors until you see something that’s unlikely to be part of an expression you can transform, like a statement. For example, say you have
returnfoo(a&&a.b&&a.b/*|*/.c);
You start on an Identifier, and walk up because an Identifier is obviously part of something you could transform. One parent up is a PropertyAccessExpression, which could also be part of a candidate, so you keep walking. Another PropertyAccessExpression, keep going. A BinaryExpression with an && token, keep going, while saving this node as a candidate expression. Now, since you have a candidate BinaryExpression, I think you’d want to continue only if the next parent is also a candidate BinaryExpression. It’s not, so you use the BinaryExpression as the potential node to transform.
I’m probably oversimplifying this in my head, but I think you at least want to make sure the refactor works for
- return statement expressions
- call arguments
- variable initializers
in addition to ExpressionStatements.
Uh oh!
There was an error while loading.Please reload this page.
…ForTriggerReason1.tsCo-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com>
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.
Sorry, I didn't have enough time to look at the refactor again, but I did review the tests and I think I found a mistake in the emit.
tests/cases/fourslash/refactorConvertToOptionalChainExpression_AccessCallCallReturnValue.ts OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalForAny.ts OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalInitialIdentifier.tsShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
...ases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanConditionalReturnKeyword.tsShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
tests/cases/fourslash/refactorConvertToOptionalChainExpression_NotForUnknown.ts OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
...ses/fourslash/refactorConvertToOptionalChainExpression_SubexpressionWithConvertiblePrefix.ts OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
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 comments on the new syntactic code, plus a suggestion for how to handlea && a.b && a.b()
.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
*/ | ||
function getLastPropertyAccessChain(node: Expression): PropertyAccessExpression | undefined { | ||
// foo && |foo.bar === 1|; - here the right child of the && binary expression is another binary expression. | ||
// the rightmost member of the && chain should be the leftmost child of that expression. | ||
if (isBinaryExpression(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.
You should alsonode = skipParentheses(node)
at the top 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.
What would be an example we care to test, something likea && (a).b
?
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,foo && (foo.bar === 1)
seems more realistic since the precedence between different binary operators can be hard to remember, but I think if you putskipParentheses
in the right places, you can trivially handlefoo && (((foo).bar) === 1)
😁
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
if (fullMatch && !isInJSFile(source)) { | ||
Debug.assert(checker.getSymbolAtLocation(source) === checker.getSymbolAtLocation(target)); | ||
} | ||
return fullMatch; |
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 there’s possibly a less efficient but more easily understandable way to write this function by going depth-first, grabbing the left-most name of both the chain and the subchain right away, and walking up their parents. But I couldn’t write an elegant example in a few minutes, so maybe it wouldn’t be as good as I think it would.
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 guess the easiest to understand way to break it down, in my mind, would be to have a function that returns the array["a", "b", "c", "d"]
for the PropertyAccessExpressiona.b.c.d
(it could return strings or Identifiers, either way), then
functionchainStartsWith(chain:EntityNameExpression,subchain:EntityNameExpression){constsubchainIdentifiers=getIdentifiersInEntityName(subchain);constchainIdentifiers=getIdentifiersInEntityName(chain);returnevery(subchain,(identifier,index)=>{returnidentifier.getText()===chainIdentifiers[index]?.getText();});}
This would be a bit more eager than what you have, and would allocate some arrays you don’t strictly need, but is easier to follow to me.
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.
In this example I also presuppose the typesEntityNameExpression
to guarantee that you have a string of dot-separated identifiers, because it makes this function easier to reason about and there are alreadyisEntityNameExpression
functions you can use, but if you wanted to nitpick on efficiency, you could check on the fly ingetIdentifiersInEntityName
, returningundefined
if you discover that you don’t actually have an EntityNameExpression.
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 this make other cases we care about easier to implement (maybe optional element access) in addition to making it easier to read? It does feel quite awkward to go through the entirety of both access chains to build the arrays if we would terminate on the first check we would have made otherwise. On the other hand, really long access chains in binary expressions are probably not common anyway?
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 expanding to optional element access should be pretty easy either way.
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.
Just a couple of tiny style suggestions left.
return undefined; | ||
function getMatchingStart(chain: Expression, subchain: Expression): PropertyAccessExpression | Identifier | undefined { | ||
if (!isIdentifier(subchain) && !isPropertyAccessExpression(subchain)) return undefined; | ||
return chainStartsWith(chain, subchain) ? subchain : 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.
Would this work as well? Seems easier to read than double!
returnchainStartsWith(chain,subchain) ?subchain :undefined; | |
return(isIdentifier(subchain)||isPropertyAccessExpression(subchain))&&chainStartsWith(chain,subchain) ?subchain :undefined; |
@@ -251,10 +234,10 @@ namespace ts.refactor.convertToOptionalChainExpression { | |||
} | |||
function doChange(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, info: Info, _actionName: string): void { | |||
const { lastPropertyAccessChain, occurrences, expression } = info; | |||
const {finalExpression:lastPropertyAccessChain, occurrences, expression } = info; |
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.
maybe just change the name tofinalExpression
here too?
🎉 |
Uh oh!
There was an error while loading.Please reload this page.
closes#35018
Adds a refactor to convert
&&
binary expressions to optional chain expressions.Supported conversions:
We offer the refactor to convert ternary expressions to nullish coalescing expressions if the type of
c
is not one ofnull
,unknown
,undefined
, orany
:An explicit request (
"invoked"
) for a 0-length span will check the largest containing binary expression for a refactor: