Skip to content
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

Merged

Conversation

alrz
Copy link
Member

@alrz alrz commented Aug 23, 2019

Fixes #38083
Fixes #37035

Also the feature is rewritten to share most logic for VB and C# as they get close to feature parity.

@alrz alrz requested review from a team as code owners August 23, 2019 14:26
@CyrusNajmabadi
Copy link
Member

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cute!

Copy link
Member

@jcouv jcouv Sep 10, 2019

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;

Copy link
Member Author

@alrz alrz Sep 10, 2019

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.

await CreateAnalyzer(syntaxFacts, semanticModel)
.ComputeRefactoringsAsync(context).ConfigureAwait(false);
var analyzer = CreateAnalyzer(context.Document.GetLanguageService<ISyntaxFactsService>());
return analyzer.ComputeRefactoringsAsync(context);
Copy link
Member

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.

Copy link
Member Author

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
Copy link
Member

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
Copy link
Member

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.

expression: (ExpressionSyntax)expression,
closeParenToken: ifStatement.CloseParenToken.WithPrependedLeadingTrivia(ElasticMarker),
openBraceToken: block?.OpenBraceToken ?? Token(SyntaxKind.OpenBraceToken),
sections: List(sectionList.OfType<SwitchSectionSyntax>()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. when do we have a section list not built out of SwitchSectionSyntax?
  2. if we have other types of nodes in that list, it looks like we're just droppin it on the floor. is that ok?

Copy link
Member Author

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)

Copy link
Member

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 :)

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;
Copy link
Member

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);
}
Copy link
Member

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));
}
Copy link
Member

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>()
Copy link
Member

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)));
Copy link
Member

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?

@alrz
Copy link
Member Author

alrz commented Oct 3, 2019

ping @jasonmalinowski
@ryzngard could you drive this PR as well? thanks

@alrz alrz requested review from CyrusNajmabadi and removed request for a team October 3, 2019 19:38
Copy link
Member

@jasonmalinowski jasonmalinowski left a 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:

  1. 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.
  2. 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):
Copy link
Member

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()
Copy link
Member

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.

//
internal abstract class Analyzer
{
private SyntaxNode? _switchTargetExpression;
Copy link
Member

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!);
Copy link
Member

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?

Copy link
Member Author

@alrz alrz Oct 9, 2019

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

Copy link
Member

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...

Copy link
Member

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...


if (!ParseIfStatementSequence(operations.Slice(1), sections, out defaultBodyOpt))
{
var nextStatement = operations[1];
Copy link
Member

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,
Copy link
Member

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;
Copy link
Member

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.

@jasonmalinowski
Copy link
Member

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! 😄

@jasonmalinowski jasonmalinowski changed the base branch from master to release/dev16.5-preview1 October 8, 2019 22:04
@alrz
Copy link
Member Author

alrz commented Oct 9, 2019

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.

@jasonmalinowski
Copy link
Member

@alrz Yeah the UI is strange -- it'll show just my comments leaving you guessing what I was responding to.

@alrz
Copy link
Member Author

alrz commented Oct 14, 2019

I think the only major remaining issue is the trivia handling, we could file a bug to track further work.

Copy link
Member

@jasonmalinowski jasonmalinowski left a 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!);
Copy link
Member

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!);
Copy link
Member

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...

@jasonmalinowski
Copy link
Member

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?

@alrz
Copy link
Member Author

alrz commented Oct 16, 2019

You can either RoslynDebug.Assert() if you want

This is the one being used in the latest iteration.

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?

It's the trivia for the guard expression that is lost (see #38238 (comment)).

@alrz
Copy link
Member Author

alrz commented Oct 16, 2019

@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...

I barely remember but I think that was for generics? .. anyways I used a non-nullable reference type initialized with null! for that.

@jasonmalinowski
Copy link
Member

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!

@jasonmalinowski jasonmalinowski merged commit ac5a49b into dotnet:release/dev16.5-preview1 Oct 16, 2019
@alrz
Copy link
Member Author

alrz commented Oct 17, 2019

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.

@jasonmalinowski
Copy link
Member

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-IDE Community The pull request was submitted by a contributor who is not a Microsoft employee. Feature Request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants