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

#45552 use string equals over string compare #5116

Conversation

NewellClark
Copy link
Contributor

Fix #45552.

Note: I've gone ahead and added a GetFirstOrDefaultMemberWithParameterTypes extension method. I've found the existing GetFirstOrDefaultMemberWithParameterInfos method to be clunky while implementing other analyzers. Also, the ParameterInfo class that the older method uses only accepts INamedTypeSymbol, and not ITypeSymbol, which has caused problems on a couple of my other analyzers.

Add GetFirstOrDefaultMemberWithParameterTypes to utilities
@NewellClark NewellClark requested a review from a team as a code owner May 19, 2021 01:53
Remove all references to SyntaxGenerator from analyzer class
Comment on lines 32 to 33
if (!RequiredSymbols.TryGetSymbols(semanticModel.Compilation, out var symbols))
return;
Copy link
Member

@Youssef1313 Youssef1313 May 19, 2021

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

Copy link
Contributor Author

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.

Copy link
Contributor

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.

var replacementNode = selector.GetReplacementExpression(operation, editor.Generator);
editor.ReplaceNode(operation.Syntax, replacementNode);
return editor.GetChangedDocument();
}, Resx.UseStringEqualsOverStringCompareCodeFixTitle);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
}, Resx.UseStringEqualsOverStringCompareCodeFixTitle);
}, nameof(Resx.UseStringEqualsOverStringCompareCodeFixTitle));

@NewellClark NewellClark marked this pull request as draft May 19, 2021 14:40
Analyzer and fixer were intertwined in unhealthy ways that also caused CI failures.
@NewellClark NewellClark marked this pull request as ready for review May 19, 2021 20:43
@NewellClark
Copy link
Contributor Author

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 SyntaxGenerator which kept triggeringRS1022. I rewrote the analyzer so it no longer has any dependencies on the fixer.

@codecov
Copy link

codecov bot commented May 19, 2021

Codecov Report

Merging #5116 (6562d14) into release/6.0.1xx (5ee4a10) will increase coverage by 0.00%.
The diff coverage is 94.45%.

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

@mavasani
Copy link
Contributor

Tagging @ryzngard for review.

CreateChangedDocument,
nameof(Resx.UseStringEqualsOverStringCompareCodeFixTitle));
context.RegisterCodeFix(codeAction, context.Diagnostics);
return;
Copy link
Contributor

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

Copy link
Member

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

Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@NewellClark NewellClark May 21, 2021

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.

Copy link
Contributor

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.


// 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;
Copy link
Contributor

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)

Copy link
Contributor Author

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 ?
Copy link
Contributor

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

Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

@NewellClark NewellClark May 21, 2021

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.

if (!RequiredSymbols.TryGetSymbols(context.Compilation, out var symbols))
return;
context.RegisterOperationAction(AnalyzeOperation, OperationKind.Binary);
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return;

Copy link
Contributor

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)

Copy link
Contributor Author

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.

}

// No IOperation instances are being stored here.
#pragma warning disable RS1008 // Avoid storing per-compilation data into the fields of a diagnostic analyzer
Copy link
Contributor

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?

Copy link
Contributor Author

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

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

Copy link
Contributor Author

@NewellClark NewellClark May 21, 2021

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.

Copy link
Contributor

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.

Copy link
Contributor

@ryzngard ryzngard left a 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants