-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Update IfToSwitch refactoring for switch expressions #38238
Update IfToSwitch refactoring for switch expressions #38238
Conversation
Overall looking good! Can review on full later today. One thing I noticed is that there's a file with a great overview on comments about the supported forms. However, I think the rest of the file could use some comments as well as each form is handled |
@@ -546,5 +546,24 @@ public static bool SequenceEqual<T>(this IEnumerable<T> first, IEnumerable<T> se | |||
|
|||
return true; | |||
} | |||
|
|||
public static T AggregateOrDefault<T>(this IEnumerable<T> source, Func<T, T, T> func) |
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!
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 simpler to assume an aggregation function that does nothing on default values, allowing us to write:
T result = default;
foreach (var x in source) { result = agg(result, x); }
return result;
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 a fold function without a seed (reduce), the initial default there doesn't work well with the use case.
would it be simpler to assume an aggregation function that does nothing on default
that would only make the definition simpler, it has to be handled by the caller.
src/EditorFeatures/CSharpTest/CodeActions/ConvertIfToSwitch/ConvertIfToSwitchTests.cs
Show resolved
Hide resolved
await CreateAnalyzer(syntaxFacts, semanticModel) | ||
.ComputeRefactoringsAsync(context).ConfigureAwait(false); | ||
var analyzer = CreateAnalyzer(context.Document.GetLanguageService<ISyntaxFactsService>()); | ||
return analyzer.ComputeRefactoringsAsync(context); |
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.
nit: consider inlining the temp and making a =>
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.
I think I'm not quite done with code structure here, the Analyzer class should probably be splitted to a few other. would appreciate any idea on that
{ | ||
Task ComputeRefactoringsAsync(CodeRefactoringContext context); | ||
} | ||
|
||
protected abstract IAnalyzer CreateAnalyzer(ISyntaxFactsService syntaxFacts, SemanticModel semanticModel); | ||
public abstract class Pattern |
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 love hte name here (yet). without a comment, i have no idea what this conveys.
where TExpressionSyntax : SyntaxNode | ||
where TIfStatementSyntax : SyntaxNode | ||
where TSwitchLabelSyntax : SyntaxNode | ||
public sealed class SwitchLabel |
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 comments. i can't tell if this is supposed to represent the code that was anlayzed, the code that is being generated, (both), etc.
...Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.cs
Outdated
Show resolved
Hide resolved
...Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.cs
Outdated
Show resolved
Hide resolved
...Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.cs
Outdated
Show resolved
Hide resolved
...Features/Core/Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.cs
Outdated
Show resolved
Hide resolved
...Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.cs
Outdated
Show resolved
Hide resolved
...Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.cs
Outdated
Show resolved
Hide resolved
expression: (ExpressionSyntax)expression, | ||
closeParenToken: ifStatement.CloseParenToken.WithPrependedLeadingTrivia(ElasticMarker), | ||
openBraceToken: block?.OpenBraceToken ?? Token(SyntaxKind.OpenBraceToken), | ||
sections: List(sectionList.OfType<SwitchSectionSyntax>()), |
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.
- when do we have a section list not built out of SwitchSectionSyntax?
- if we have other types of nodes in that list, it looks like we're just droppin it on the floor. is that ok?
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 not changed in this PR. it should just use Cast (or generic if possible)
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 somewhat see thsi change as near a total rewrite. So not a bad time to jsut clean these things up as well :)
...Features/CSharp/Portable/ConvertIfToSwitch/CSharpConvertIfToSwitchCodeRefactoringProvider.cs
Outdated
Show resolved
Hide resolved
var requiresBlock = !_semanticModel.AnalyzeDataFlow(node).VariablesDeclared.IsDefaultOrEmpty; | ||
var node = operation.Syntax; | ||
var requiresBreak = operation.SemanticModel.AnalyzeControlFlow(node).EndPointIsReachable; | ||
var requiresBlock = !operation.SemanticModel.AnalyzeDataFlow(node).VariablesDeclared.IsDefaultOrEmpty; |
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.
can this method not somehow be shared between VB and C#?
case CastExpressionSyntax castExpression: | ||
return castExpression.Expression; | ||
return expression is null ? null : WhenClause(expression); | ||
} |
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.
nit: =>
return CasePatternSwitchLabel( | ||
AsPatternSyntax(label.Pattern), | ||
AsWhenClause(label), | ||
Token(SyntaxKind.ColonToken)); | ||
} |
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.
nit: =>
can we make this return a TSwitchLabelSyntax?
{ | ||
var block = ifStatement.Statement as BlockSyntax; | ||
return AsWhenClause(label.Guards | ||
.Cast<ExpressionSyntax>() |
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 you know all the guards are expressions, can we make it TExpressionSyntax?
var block = ifStatement.Statement as BlockSyntax; | ||
return AsWhenClause(label.Guards | ||
.Cast<ExpressionSyntax>() | ||
.AggregateOrDefault((prev, current) => BinaryExpression(SyntaxKind.LogicalAndExpression, current.WalkDownParentheses(), prev))); |
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 you walk down parens, could you not screw up precedence in some way?
ping @jasonmalinowski |
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.
Only real questions here are:
- A suspicious ! to suppress a nullable warning that wasn't obvious why it was the case and could either use a comment as to why it's safe or just a Contract.ThrowIfNull() if it's not obvious.
- Why a test that was asserting whitespace isn't doing that anymore. I can't say I really care about the whitespace scenario but do worry if we're dropping other kinds of trivia somehow.
Otherwise most of my comments are just requests to convert regular comments to doc comments since it's nice when navigating around to be able to hover and see explanations for things. @alrz I'm happy to fix those up if you don't have the time as penance for not reviewing this soon enough.
@@ -341,7 +341,7 @@ void M(object o) | |||
{ | |||
switch (o) | |||
{ | |||
case string s when s.Length > 5 && s.Length < 10: | |||
case string s when (s.Length > 5 && s.Length < 10): |
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.
Why did it get worse though? I would have assumed the parenthesis remover was previously running?
@@ -380,7 +380,7 @@ void M(object o) | |||
} | |||
|
|||
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsConvertIfToSwitch)] | |||
public async Task TestPreserveTrivia() | |||
public async Task TestComplexExpression_01() |
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 original test was trying to test for maintaining whitespace, is that something we're considering not necessary any more? I see there's plenty of tests dealing with non-whitespace trivia so if you're not worried then I'm not worried.
src/EditorFeatures/CSharpTest/CodeActions/ConvertIfToSwitch/ConvertIfToSwitchTests.cs
Outdated
Show resolved
Hide resolved
// | ||
internal abstract class Analyzer | ||
{ | ||
private SyntaxNode? _switchTargetExpression; |
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 might have been a good use for a doc comment so it's friendly to quick info)
sections.Add(new AnalyzedSwitchSection(labels: default, defaultBodyOpt, defaultBodyOpt.Syntax)); | ||
} | ||
|
||
return (sections.ToImmutableAndFree(), _switchTargetExpression!); |
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.
Why the ! here on _switchTargetExpression?
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.
_switchTargetExpression is initially unset be we don't want to allow assigning null values, plus, at the end it is expected to be set.
I think SyntaxNode _switchTargetExpression = null!
fits the above requirements?
But we need to assert anyways though I think it should be a DEBUG assertion - we don't have access to RoslynDebug
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 can either RoslynDebug.Assert() if you want or you can use Contract.ThrowIfNull(). I have no problem having a release assert in that I'm not entirely confident this would not crash if this was null...
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.
@sharwell also has a nice way you can annotate this as "it might be null to start but can't be assigned null" but I forget what it is off hand...
...Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.AnalyzedNodes.cs
Outdated
Show resolved
Hide resolved
...Portable/ConvertIfToSwitch/AbstractConvertIfToSwitchCodeRefactoringProvider.AnalyzedNodes.cs
Outdated
Show resolved
Hide resolved
|
||
if (!ParseIfStatementSequence(operations.Slice(1), sections, out defaultBodyOpt)) | ||
{ | ||
var nextStatement = operations[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.
The second bit "if there was no if/else then it would have returned false" might warrant a comment in the code here. (The question about Slice we can just call a learning opportunity for @CyrusNajmabadi and I.)
{ | ||
return operatorKind switch | ||
{ | ||
LessThan => GreaterThan, |
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.
Honestly, at first I went "uh..." until I saw @CyrusNajmabadi's that indicated this was probably intentional. A doc comment of "changes the direction the operator is pointing" or something is probably for the best.
} | ||
|
||
public override bool HasUnreachableEndPoint(IOperation operation) | ||
=> !operation.SemanticModel.AnalyzeControlFlow(operation.Syntax).EndPointIsReachable; |
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.
Why does VB need the first/last one? Not sure if this is really an API request to the compiler team in disguise.
Also @alrz given this is more or less a rewrite of the feature and has enough churn, at this point per our internal bar we'd be targeting this for 16.5 instead of 16.4. The code looks great but that's a bar we'd be applying to any newer feature work like this at this point. I'll update the targeted branch but shouldn't need any other work from you at this point. Let me know if that causes any problems or concerns for you. As usual, great code! 😄 |
It's weird I can't reply to some of review comments - there's no comment box. Is it possible that you have reviewed an older commit? ah nevermind, you were replying to the previous reviews but github shows another diff here. |
@alrz Yeah the UI is strange -- it'll show just my comments leaving you guessing what I was responding to. |
I think the only major remaining issue is the trivia handling, we could file a bug to track further work. |
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.
Changes looking good but @alrz did you want to add an assert for that null suppression? That one worries me since it's definitely not "obvious"...
sections.Add(new AnalyzedSwitchSection(labels: default, defaultBodyOpt, defaultBodyOpt.Syntax)); | ||
} | ||
|
||
return (sections.ToImmutableAndFree(), _switchTargetExpression!); |
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 can either RoslynDebug.Assert() if you want or you can use Contract.ThrowIfNull(). I have no problem having a release assert in that I'm not entirely confident this would not crash if this was null...
sections.Add(new AnalyzedSwitchSection(labels: default, defaultBodyOpt, defaultBodyOpt.Syntax)); | ||
} | ||
|
||
return (sections.ToImmutableAndFree(), _switchTargetExpression!); |
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.
@sharwell also has a nice way you can annotate this as "it might be null to start but can't be assigned null" but I forget what it is off hand...
And oh yeah regarding trivia: I don't see you breaking any tests, so do we think we really made anything worse or it's as "bad" as it ever was? |
This is the one being used in the latest iteration.
It's the trivia for the guard expression that is lost (see #38238 (comment)). |
I barely remember but I think that was for generics? .. anyways I used a non-nullable reference type initialized with |
Ah sorry, for some reason I still saw the old version. I think this PR at this point is pushing GitHub's limits in terms of UI... In any case, awesome work for all of this, happy to merge! |
I found an issue with this when subsequent blocks are not handled properly (they are not removed) but it's unlikely to happen in code and it won't crash anyways. I will add more tests and fix possible issues whenever I could. |
@alrz Thanks for the heads up, it happens! Even if you can't get the code fix, file a bug quickly just so we can track it and have somebody else take a look if necessary. |
Fixes #38083
Fixes #37035
Also the feature is rewritten to share most logic for VB and C# as they get close to feature parity.