-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Add codestyle to handle unnecessary parentheses. #21316
Add codestyle to handle unnecessary parentheses. #21316
Conversation
Ready for initial review. Need to do VB side. |
Tagging @dotnet/roslyn-ide |
2bc1137
to
4fd7d7c
Compare
Tagging @DustinCampbell |
f821b3d
to
c00b31b
Compare
@DustinCampbell User option looks like this: 'Always' means we don't touch any parens and we don't give you any notification about them. "When clarifying precedence" means we'll ignore extraneous parens in arithmetic statements (i.e. we won't notify you about Tagging @kuhlenh as well. |
tagging @Pilchie |
1eccca5
to
65b8786
Compare
I'm pretty strongly opposed to the inclusion of |
I disagree. As with any of our options, users can choose to opt into styles of coding that others may feel reduce quality and may potentially ease the introduction of bugs into code. This is, for example, why we have two different coding styles in Roslyn itself as there are styles we offer that this team believes have a negative effect on quality. Similarly, the usage of "out vars" as we use them (and the code style feature we offer around them) is one that several community members mentioned they absolutely would not use and felt would lower code quality. That's why these are options and styles. They're not for everyone. They're made available so that people who happen to prefer such an approach have the facility to have the IDE handle it for them. I see no reason to make people go to a third party analyzer here when this is a reasonable thing for them to want for their own code. Tagging @kuhlenh |
@dpoeschl This now has our formalization and i would like your eyes here. Not implemented (but hopefully will be tonight): Adding in required parentheses based on hte users' options. |
@dpoeschl have added fixer to add parentheses as necessary |
Feature appears to the user (with these defaults) as: The idea is, by default, we don't require, or try to remove, parentheses in logical or arithmetic operations. However, by default we will fade out unnecessary parens for other situations. i.e. if the user writes The options for 'arithmetic' and 'logical' are both:
We've separated out arithmetic and logical operations as there has been feedback (i.e. from @dpoeschl`) that he's fine with arithmetic operations as is, but would personally like to be able to opt into stricter rules around how The options for 'other' are: |
@dotnet/roslyn-ide This is ready for review. |
|
||
var additionalLocations = ImmutableArray.Create(parenthesizedExpression.GetLocation()); | ||
context.ReportDiagnostic( | ||
Diagnostic.Create(this.UnnecessaryWithSuggestionDescriptor, parenthesizedExpression.GetFirstToken().GetLocation(), additionalLocations)); |
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 understand the desire for the parentheses to be grayed out, but this doesn't honor the user's chosen severity. If I want these to be warnings, then I set the severity to "Warning" and that choice is ignored. With things like Conditional Delegate Call, we gray out the unused bit but also honor the chosen Severity by squiggling the invocation.
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 has been fixed.
? arithmeticPreference.Notification.Value | ||
: logicalPreference.Notification.Value; | ||
|
||
var operatorToken = binaryExpression.ChildNodesAndTokens()[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.
I think this will end up being a bit weird. If you have a || b && c && d
, this will squiggle only the final &&
even though it plans to parenthesize all of (b && c && d)
. I mean, it makes sense when you think about it, but to someone who doesn't automatically visualize syntax trees that correctly honor left associativity, it might be misleading. Should we have one diagnostic bug squiggle all of the operators that will get grouped together? How's the editor support of that?
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 has been changed.
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.
Fixed.
{ | ||
void M() | ||
{ | ||
int x = a || b [||]&& c; |
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.
Wow, the test caret position [||]
is really hard to read in this scenario. Can we switch to $$ (since it's a diagnostic and not a refactoring)?
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.
unfortunately, that would take some deep manipulation of hte unit test harness. i'm ok keeping as is :)
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.
Actually, turns out it was easy. So i've added support for that and changed all these tests to use $$ now.
} | ||
|
||
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsAddRequiredParentheses)] | ||
public async Task TestNotIfArithmeticPrecedenceIsOff() |
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 had a hard time parsing the *IsOff()
unit test names. Perhaps *IsNotEnforced
?
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.
Done.
{ | ||
void M() | ||
{ | ||
int x = 1 + 2 [||]+ 3; |
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.
Wait, even if Arithmetic Precedence enforcement is on, this still wouldn't report anything so this test isn't actually testing anything, right? (Identical code to the previous test that was already showing it not reporting with everything enabled... so of course it wouldn't report with a subset of things enabled.)
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 keeping this test (just for exhaustiveness), but i'm adding another test that actually verifies that we don't add teh parens where we normally would if hte optoin was on.
|
||
if (!ShouldRemoveParentheses()) | ||
{ | ||
// Parentheses should not be removed. |
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.
Unnecessary comment?
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.
Updated.
@@ -42,6 +58,21 @@ static EnumCodeStyleOptionViewModel() | |||
OptionSet options, | |||
string groupName, | |||
List<CodeStylePreference> preferences) | |||
: this(option, null, description, enumValues, previews, 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.
language: null
(I know it's annoying to have to do all the other ones too... For now...)
} | ||
|
||
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsAddRequiredParentheses)] | ||
public async Task TestNotIfLogicalPrecedenceIsOff() |
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.
Same as the other "IsOff", I don't think it's testing anything.
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.
Addressed.
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.
addressed
} | ||
|
||
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsRemoveUnnecessaryParentheses)] | ||
public async Task TestArithmeticNotRequiredForClarityWhenPrecedenceStaysTheSame1() |
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.
As discussed elsewhere, the precedence doesn't change but the order of evaluation does, which can be dangerous in several cases.
// -- even if they could be removed depending on whether the parenthesized | ||
// expression appears on the left or right side of the parent binary expression. | ||
|
||
// First, does the binary expression result in an operator overload being |
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.
Test for 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.
Sure
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.
tests were added for this.
This is a case that probably needs to be fixed before it could be adopted: It wants to add parentheses to Discussed offline: maybe logical means "both the parent and child are logical; same with arithmetic (a > b || c < d)" |
➡️ Let's remove the two options "Remove if unnecessary" from both arithmetic and logical. We can discuss their addition in a later release, but at this time I remain so strongly opposed to their existence (even though they are not the default) that it would appear to undermine the feature as a whole. |
StyleCop Analyzers had a user hit this case in the wild (DotNetAnalyzers/StyleCopAnalyzers#1284). Part of the reason I keep suggesting new features start with the existing test cases we have, because they cover many things like this. Edit: I see, the interesting thing here is that one set of parentheses can be removed, but not both. We had a similar case arise with converting |
I would prefer to keep them. I think it's fine to allow users to opt into this level of cleanup for their own code. |
There's no rush here. Let's wait the weekend and have a discussion about it on Monday. |
0bd55e3
to
752be05
Compare
{ | ||
internal static class SyntaxEditorExtension | ||
{ | ||
public static async Task ApplySemanticEditsAsync<TNode>( |
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.
Tagging @sharwell
The pull request adds more than 1,000 different configurations. It's overwhelming, and worse, looking at the options I have no idea what the selections will actually mean for code. Can you provide examples of why the following simplified configuration would not be sufficient?
|
Primarily, because i think it would be too coarse-grained. For example, let's look at this approach:
this means if i'm a person who is totally fine with differing arithmetic precedences (i.e. "i'm fine with It seems like a very big pill to have to swallow if you had to say "i must make all precedence explicit, when really there are only a few cases that i find myself caring about in my project".
:) It's worth noting that we add, effectively, the same number of configs that resharper adds. You specify which precedences you care about, and you specify if you want the parens for clarity, or you don't want them. They happen to have a nicer UI for this (through the use of a good UI widget), but they allow you to be clear about where you want the tool involved, and where you'd like it left out. -- Additional notes:
This i feel safe about (and is how the current PR is set up). The 'other' category is really for cases where there is no precedence concept involved at all, and which you can generally feel very safe about removing while not impacting clarity.
That's close to how things are today, except if you have something like |
I think it's worth noting that we have previews for these options, which help explain their meaning. They were also named to help explain how they woudl work. i.e. "dotnet_style_parentheses_in_logical_expressions:always_for_clarity/never_if_unnecessary". We tried to make it so that this read in a way that would make sense to someone. i.e. "in a logical expression, always require parentheses for clarity" or "in a logical expression, never allow parentheses that are unnecessary". ergo, if i have some logical operator "a op b" then this controls if i would want (or not want) parentheses inside the op to clarify precedence. The preview demosntrates this by showing the following for 'always_for_clarity':
And the following for 'never_if_unnecessary':
|
Could you clarify this bit? Why would null-coalescing be special cased? |
Also, just to be clear: i've very interested in your idea, and i welcome approaches that can potentially simplify things here. If we can reformulate this in a better way, i'm all ears. The above points were just my initial concerns about your proposal. But it's super interesting and maybe with some tweaks could work. FWIW, i started with an initial approach that was similar to what you had. However, i almost immediately disliked it because of my own personal preferences around where i find precedence parens useful, vs where i personally do not prefer them. @dpoeschl and i went back and forth on a bunch of ideas, but we eventually settled on wanting to try out the 'fine-grained' approach, because it truly was a consistent (if potentially overwhelming) of providing this functionality. |
This sounds like a valuable trade-off. It eliminates an edge case that few users will care strongly about for a dramatic reduction in complexity.
Sure, this is functionally equivalent to what I was saying so no objection there.
We can do this at severity 'suggestion'. Same as we did for using collection and object initializers.
That works for me. 👍 |
It would certainly be something we can try out! I would like peopel to then use this and to see if this felt ok. My gut feeling is that it will be too coarse grained... but i woudl be happy to be proven wrong! :) |
I would be very much against having "clarify precedence" be the default with any severity greater than none. If there is no "ignore" option then it has to be one or the other but it should definitely be set to none. People have a lot of differing opinions about this and we can't assume they want 1000s of suggestions in their code every time they used an operator after upgrading. |
This is interesting to me for three reasons:
|
A question on something i missed earlier:
Was it intentional that there as no "remove if unnecessary here"?
|
perhaps. it might be interesting ot analyze our own codebase, and see (for example) the percentages on things like "parentheses in arithmetic" versus "parentheses in logical". i.e. in arithmetic expression with differing precedence, what percentage of the time are those parenthesed unnecessarily. And then do the same check for logical. My gut feeling is that we'll find that logical ops are parenthesized far more often. even in our codebase where we're skilled in this domain and we really know the difference. But even for our own team, we find parens for one more valuable and more common than parens for the other. |
Yes, this was intentionally omitted.
We do have a downside that mixed-precedence is generally discouraged in this codebase for ongoing development. |
Could you clarify how that is a downside? I think it's extremely interesting that even in roslyn (which likely has the largest set of experts in the languages), we've chosen to still want some of our expressions to err on the side of clarity for certain operations. |
@sharwell @Neme12 and me discussed this at length in gitter: https://gitter.im/dotnet/roslyn?at=5ac675a42b9dfdbc3a5d3477 the takeaway is that there are three important design decisions that need to be made. The design decisions are:
@sharwell will present this at the next IDE design meeting. |
my preferences are:
|
@CyrusNajmabadi @sharwell - I am in favor of "fine-grained (7)" because we don't have a good story for versioning .editorconfig options if we wanted to change this later. That said, I can be convinced to do "coarse (2)" if we can show there is no difference via something like this. And agree with Cyrus's last 2 points in the post above this. |
Also worth having for the conversation. Here is how we present things to the user in tools|options wrt to preview: And how we discourage people from certain options that are likely going to lead to very unintuitive results: If you pick either coarse/coarsest, i would like some thought put into how we present the information to the user in a clear manner. Thanks! |
Closing out in favor of #26013 |
Fixes #25554