-
Notifications
You must be signed in to change notification settings - Fork 468
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
#45552 use string equals over string compare #5116
#45552 use string equals over string compare #5116
Conversation
Add GetFirstOrDefaultMemberWithParameterTypes to utilities
…-string-equals-over-string-compare
Remove all references to SyntaxGenerator from analyzer class
...Analyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.Fixer.cs
Outdated
Show resolved
Hide resolved
...Analyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.Fixer.cs
Outdated
Show resolved
Hide resolved
if (!RequiredSymbols.TryGetSymbols(semanticModel.Compilation, out var symbols)) | ||
return; |
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.
How can this fail? This was already checked in the analyzer. Consider an assertion
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.
Do we know for a fact that it is impossible for this to fail in the fixer after it succeeded in the analyzer? I figured better safe than sorry.
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.
Agree this should be an assertion. If the symbols somehow disappear between the analyzer and the fixer something has gone horribly wrong.
...Analyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.Fixer.cs
Outdated
Show resolved
Hide resolved
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.cs
Outdated
Show resolved
Hide resolved
var replacementNode = selector.GetReplacementExpression(operation, editor.Generator); | ||
editor.ReplaceNode(operation.Syntax, replacementNode); | ||
return editor.GetChangedDocument(); | ||
}, Resx.UseStringEqualsOverStringCompareCodeFixTitle); |
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.
}, Resx.UseStringEqualsOverStringCompareCodeFixTitle); | |
}, nameof(Resx.UseStringEqualsOverStringCompareCodeFixTitle)); |
...Analyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.Fixer.cs
Outdated
Show resolved
Hide resolved
Analyzer and fixer were intertwined in unhealthy ways that also caused CI failures.
I ended up having to completely rewrite the analyzer because I wasn't aware that analyzers are not supposed to have any reference whatsoever to certain APIs, and originally both the analyzer and fixer had access to a type that used |
Codecov Report
@@ Coverage Diff @@
## release/6.0.1xx #5116 +/- ##
=================================================
Coverage 95.59% 95.59%
=================================================
Files 1220 1223 +3
Lines 279887 280320 +433
Branches 16818 16841 +23
=================================================
+ Hits 267548 267965 +417
- Misses 10077 10092 +15
- Partials 2262 2263 +1 |
…-string-equals-over-string-compare
…-string-equals-over-string-compare
Tagging @ryzngard for review. |
...Analyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.Fixer.cs
Outdated
Show resolved
Hide resolved
...Analyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.Fixer.cs
Outdated
Show resolved
Hide resolved
CreateChangedDocument, | ||
nameof(Resx.UseStringEqualsOverStringCompareCodeFixTitle)); | ||
context.RegisterCodeFix(codeAction, context.Diagnostics); | ||
return; |
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.
return is not needed 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.
@ryzngard It's a convention used in the repository (and dotnet/roslyn as well) to put an explicit return just before local functions
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.
o.o who decided that? Because I definitely haven't been
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.
Apparently this is @CyrusNajmabadi preferrence... I won't block a PR on style preference that isn't documented. You can keep if you like, and I'll just grumble in my corner alone :P
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've added https://github.com/dotnet/roslyn-analyzers/issues/5122 for further discussion
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.
@mavasani has asked me to have a return
before the local function definitions for clarity.
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.
Yea, I followed up offline with him and a few others. I was unaware that was a practice we started. Hopefully we can get an analyzer to help the underlying issue, but for now this is correct.
...Analyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.Fixer.cs
Outdated
Show resolved
Hide resolved
|
||
// We know that the 'ignoreCase' argument in 'string.Compare(string, string, bool)' is a boolean literal | ||
// because we've asserted that 'IsMatch' returns true. | ||
var ignoreCaseLiteral = (ILiteralOperation)compareInvocation.Arguments.GetArgumentForParameterAtIndex(2).Value; |
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 true for cases where a boolean variable is used? I haven't checked UseStringEqualsOverStringCompare.IsStringStringBoolCase
to know if it handles that as well
bool CompareNames(Person p, bool ignoreCase) => string.Compare(Name, p.Name, ignoreCase)
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 analyzer only flags cases where the boolean argument is a literal.
// replace it with a call to 'string.Equals(x, y, StringComparison.CurrentCultureIgnoreCase)'. | ||
// If the violation contains a call to 'string.Compare(x, y, false)' then we | ||
// replace it with a call to 'string.Equals(x, y, StringComparison.CurrentCulture)'. | ||
var stringComparisonEnumMemberName = (bool)ignoreCaseLiteral.ConstantValue.Value ? |
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 should likely be Ordinal
and OrdinalIgnoreCase
based on guidelines
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.
@ryzngard string.Compare is using CurrentCulture by default.
https://source.dot.net/#System.Private.CoreLib/String.Comparison.cs,200
So probably the codefix would want to keep the same semantics.
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 don't fully agree. In general we don't want to change explicit semantics for code, but there's no way to know if the user intended to use CurrentCulture
or not. When it's ambiguous, I think suggesting the public documented guidelines would be a good thing.
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 examples in the issue specified that we use CurrentCulture
and CurrentCultureIgnoreCase
so we don't change semantics.
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 don't see any explicit discussion in the issue about whether the replacement for string.Compare(s1, s2)
should use CurrentCulture
or Ordinal
. That said, I'm not strong enough of the opinion that we should change the functional meaning for potential correction. Especially if others think it should be exact parity.
I will expand on my thought process for discussion, but consider this non-blocking:
https://docs.microsoft.com/en-us/dotnet/standard/base-types/best-practices-strings specifies
"Use overloads that explicitly specify the string comparison rules for string operations. Typically, this involves calling a method overload that has a parameter of type StringComparison."
If I were a user trying to update code to follow that guideline, how would I consider doing it? I think most cases I would add a Ordinal
or OrdinalIgnoreCase
as the intent for string comparison unless context dictates I should be using culture aware comparison. That also follows the guidance of
"Use StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase for comparisons as your safe default for culture-agnostic string matching."
and
"Use comparisons with StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase for better performance."
This fix is listed as Usage
, but the key findings in the issue are centered around performance. Probably because nobody disagreed that string.Equals(...)
is more readable than string.Compare(...) == 0
.
If we are encouraging users to have mostly correct and performant code, or even for what I would consider to give as guidance as "probably correct" in most cases, we would use Ordinal
or OrdinalIgnoreCase
.
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 my opinion, a code fix should only change the issue that its analyzer is reporting on. This analyzer is not flagging incorrect uses of CurrentCulture
, it is flagging uses of a total-order comparison when a simple equality check was desired. I would not be opposed to an analyzer that flags usages of any Compare
or Equals
overload that doesn't specify the kind of comparison, but I don't think this analyzer should be changing semantics.
If analyzers that appear to be purely about style or performance start suggesting code fixes that quietly change semantics, as a developer I'm going to be much more wary of using code fixes.
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.cs
Outdated
Show resolved
Hide resolved
if (!RequiredSymbols.TryGetSymbols(context.Compilation, out var symbols)) | ||
return; | ||
context.RegisterOperationAction(AnalyzeOperation, OperationKind.Binary); | ||
return; |
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.
return; |
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.
Feel free to ignore per #5116 (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.
Same as above. @mavasani has asked that I return before local function declarations.
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.cs
Outdated
Show resolved
Hide resolved
src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseStringEqualsOverStringCompare.cs
Outdated
Show resolved
Hide resolved
} | ||
|
||
// No IOperation instances are being stored here. | ||
#pragma warning disable RS1008 // Avoid storing per-compilation data into the fields of a diagnostic analyzer |
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 there a bug for this false positive on RS1008?
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.
|
||
[Theory] | ||
[MemberData(nameof(CS_StringCompareExpressionsTestData))] | ||
public Task StringCompareResult_CompareToNonLiteralZero_NoDiagnostic_CS(string 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.
For const values we could also provide a diagnostic. I'd consider this an area for future improvement and not required for this PR
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 I write an expression with a constant in it, I expect that changing the value initially assigned to the constant will change the value of the expression. The fixer removes the constant from the expression entirely, which could break if the constant is 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.
While true, it's an indicator of what I would consider hard to read code and potentially bad design. I don't know of a strong use case where we would want to encourage that, and analyzers are opinionated by design.
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.
Looks good to me other than discussion points, but nothing blocking. Thanks for adding this!
Fix #45552.
Note: I've gone ahead and added a
GetFirstOrDefaultMemberWithParameterTypes
extension method. I've found the existingGetFirstOrDefaultMemberWithParameterInfos
method to be clunky while implementing other analyzers. Also, theParameterInfo
class that the older method uses only acceptsINamedTypeSymbol
, and notITypeSymbol
, which has caused problems on a couple of my other analyzers.