Skip to content

Commit 8611d76

Browse files
authored
Address CA1862 cases to diagnose and to fix and improve messages (#6928)
* Fix incorrect test cases, add missing tests, add comments. * Ensure the tests for "diagnostic but no fix" are actually not fixing it. * Improve messages * More test fixes * Modify the analyzer and fixer logic, still need to figure out parenthesis walk up * Apply suggestions * Fix formatting * msbuild -t:pack and xlfs * Additional test adjustments. * Fix parenthesis handling. * Address roslyn suggestions * Fix roslyn suggestions * Fix roslyn suggestions * Fix test cases for IndexOf with named unordered arguments. * Use string constant instead of loading it. * Revert the code fix title prefixes. Move the Equals one next to the other two. * Update messages with suggestions
1 parent 6bf9333 commit 8611d76

24 files changed

+903
-527
lines changed

src/NetAnalyzers/CSharp/Microsoft.NetCore.Analyzers/Performance/CSharpRecommendCaseInsensitiveStringComparisonFixer.cs

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,70 +17,91 @@ namespace Microsoft.NetCore.CSharp.Analyzers.Performance
1717
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
1818
public sealed class CSharpRecommendCaseInsensitiveStringComparisonFixer : RecommendCaseInsensitiveStringComparisonFixer
1919
{
20-
protected override IEnumerable<SyntaxNode> GetNewArgumentsForInvocation(SyntaxGenerator generator, string caseChangingApproachValue, IInvocationOperation mainInvocationOperation,
21-
INamedTypeSymbol stringComparisonType, out SyntaxNode? mainInvocationInstance)
20+
protected override IEnumerable<SyntaxNode> GetNewArgumentsForInvocation(SyntaxGenerator generator,
21+
string caseChangingApproachValue, IInvocationOperation mainInvocationOperation, INamedTypeSymbol stringComparisonType,
22+
string? leftOffendingMethod, string? rightOffendingMethod, out SyntaxNode? mainInvocationInstance)
2223
{
23-
List<SyntaxNode> arguments = new();
24-
bool isAnyArgumentNamed = false;
25-
2624
InvocationExpressionSyntax invocationExpression = (InvocationExpressionSyntax)mainInvocationOperation.Syntax;
2725

28-
bool isChangingCaseInArgument = false;
2926
mainInvocationInstance = null;
3027

3128
if (invocationExpression.Expression is MemberAccessExpressionSyntax memberAccessExpression)
3229
{
33-
ExpressionSyntax internalExpression = memberAccessExpression.Expression is ParenthesizedExpressionSyntax parenthesizedExpression ?
34-
parenthesizedExpression.Expression :
35-
memberAccessExpression.Expression;
30+
ExpressionSyntax internalExpression = memberAccessExpression.Expression;
31+
while (internalExpression is ParenthesizedExpressionSyntax parenthesizedExpression)
32+
{
33+
internalExpression = parenthesizedExpression.Expression;
34+
}
3635

37-
if (internalExpression is InvocationExpressionSyntax internalInvocationExpression &&
38-
internalInvocationExpression.Expression is MemberAccessExpressionSyntax internalMemberAccessExpression)
36+
if (leftOffendingMethod != null &&
37+
internalExpression is InvocationExpressionSyntax internalInvocationExpression &&
38+
internalInvocationExpression.Expression is MemberAccessExpressionSyntax internalMemberAccessExpression &&
39+
internalMemberAccessExpression.Name.Identifier.Text == leftOffendingMethod)
3940
{
41+
// We know we have an instance invocation that is an offending method, retrieve just the instance
4042
mainInvocationInstance = internalMemberAccessExpression.Expression;
4143
}
4244
else
4345
{
4446
mainInvocationInstance = memberAccessExpression.Expression;
45-
isChangingCaseInArgument = true;
4647
}
4748
}
4849

49-
foreach (ArgumentSyntax node in invocationExpression.ArgumentList.Arguments)
50-
{
51-
string? argumentName = node.NameColon?.Name.Identifier.ValueText;
52-
isAnyArgumentNamed |= argumentName != null;
50+
List<SyntaxNode> arguments = new();
51+
bool isAnyArgumentNamed = false;
5352

54-
ExpressionSyntax argumentExpression = node.Expression is ParenthesizedExpressionSyntax argumentParenthesizedExpression ?
55-
argumentParenthesizedExpression.Expression :
56-
node.Expression;
53+
foreach (IArgumentOperation arg in mainInvocationOperation.Arguments)
54+
{
55+
SyntaxNode newArgumentNode;
5756

58-
MemberAccessExpressionSyntax? argumentMemberAccessExpression = null;
59-
if (argumentExpression is InvocationExpressionSyntax argumentInvocationExpression)
57+
// When accessing the main invocation operation arguments, the bottom operation is retrieved
58+
// so we need to go up until we find the actual argument syntax ancestor, and skip through any
59+
// parenthesized syntax nodes
60+
SyntaxNode actualArgumentNode = arg.Syntax;
61+
while (actualArgumentNode is not ArgumentSyntax)
6062
{
61-
argumentMemberAccessExpression = argumentInvocationExpression.Expression as MemberAccessExpressionSyntax;
63+
actualArgumentNode = actualArgumentNode.Parent!;
6264
}
6365

64-
SyntaxNode newArgumentNode;
65-
if (isChangingCaseInArgument)
66+
string? argumentName = ((ArgumentSyntax)actualArgumentNode).NameColon?.Name.Identifier.ValueText;
67+
isAnyArgumentNamed |= argumentName != null;
68+
69+
// The arguments could be named and out of order, so we need to detect the string parameter
70+
// and remove the offending invocation if there is one
71+
if (rightOffendingMethod != null && arg.Parameter?.Type?.Name == StringTypeName)
6672
{
67-
if (argumentMemberAccessExpression != null)
73+
ExpressionSyntax? desiredExpression = null;
74+
if (arg.Syntax is ArgumentSyntax argumentExpression)
75+
{
76+
desiredExpression = argumentExpression.Expression;
77+
while (desiredExpression is ParenthesizedExpressionSyntax parenthesizedExpression)
78+
{
79+
desiredExpression = parenthesizedExpression.Expression;
80+
}
81+
}
82+
else if (arg.Syntax is InvocationExpressionSyntax argumentInvocationExpression)
83+
{
84+
desiredExpression = argumentInvocationExpression;
85+
}
86+
87+
if (desiredExpression is InvocationExpressionSyntax invocation &&
88+
invocation.Expression is MemberAccessExpressionSyntax argumentMemberAccessExpression)
6889
{
6990
newArgumentNode = argumentName == RCISCAnalyzer.StringParameterName ?
70-
generator.Argument(RCISCAnalyzer.StringParameterName, RefKind.None, argumentMemberAccessExpression.Expression) :
71-
generator.Argument(argumentMemberAccessExpression.Expression);
91+
generator.Argument(RCISCAnalyzer.StringParameterName, RefKind.None, argumentMemberAccessExpression.Expression) :
92+
generator.Argument(argumentMemberAccessExpression.Expression);
7293
}
7394
else
7495
{
75-
newArgumentNode = node;
96+
newArgumentNode = arg.Syntax;
7697
}
7798
}
7899
else
79100
{
80-
newArgumentNode = node;
101+
newArgumentNode = arg.Syntax;
81102
}
82103

83-
arguments.Add(newArgumentNode.WithTriviaFrom(node));
104+
arguments.Add(newArgumentNode.WithTriviaFrom(arg.Syntax));
84105
}
85106

86107
Debug.Assert(mainInvocationInstance != null);

src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2024,29 +2024,29 @@ Widening and user defined conversions are not supported with generic types.</val
20242024
<data name="RecommendCaseInsensitiveStringComparerStringComparisonCodeFixTitle" xml:space="preserve">
20252025
<value>Use the 'string.{0}(string, StringComparison)' overload</value>
20262026
</data>
2027-
<data name="RecommendCaseInsensitiveStringEqualsCodeFixTitle" xml:space="preserve">
2028-
<value>Use 'string.Equals(string, StringComparison)'</value>
2029-
</data>
20302027
<data name="RecommendCaseInsensitiveStringComparerDescription" xml:space="preserve">
2031-
<value>Avoid calling 'ToLower', 'ToUpper', 'ToLowerInvariant' and 'ToUpperInvariant' to perform case-insensitive string comparisons when using 'CompareTo', because they lead to an allocation. Instead, use 'StringComparer' to perform case-insensitive comparisons.</value>
2028+
<value>Avoid calling 'ToLower', 'ToUpper', 'ToLowerInvariant' and 'ToUpperInvariant' to perform case-insensitive string comparisons when using 'CompareTo', because they lead to an allocation. Instead, use 'StringComparer' to perform case-insensitive comparisons. Switching to using 'StringComparer' might cause subtle changes in behavior, so it's important to conduct thorough testing after applying the suggestion. Additionally, if a culturally sensitive comparison is not required, consider using 'StringComparer.OrdinalIgnoreCase'.</value>
20322029
</data>
20332030
<data name="RecommendCaseInsensitiveStringComparerMessage" xml:space="preserve">
2034-
<value>Prefer using 'StringComparer' to perform a case-insensitive comparison</value>
2031+
<value>Prefer using 'StringComparer' to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparer.OrdinalIgnoreCase'</value>
2032+
</data>
2033+
<data name="RecommendCaseInsensitiveStringEqualsCodeFixTitle" xml:space="preserve">
2034+
<value>Use 'string.Equals(string, StringComparison)'</value>
20352035
</data>
20362036
<data name="RecommendCaseInsensitiveStringEqualsDescription" xml:space="preserve">
2037-
<value>Avoid calling 'ToLower', 'ToUpper', 'ToLowerInvariant' and 'ToUpperInvariant' to perform case-insensitive string comparisons, as in 'string.ToLower() == string.ToLower()', because they lead to an allocation. Instead, use 'string.Equals(string, StringComparison)' to perform case-insensitive comparisons.</value>
2037+
<value>Avoid calling 'ToLower', 'ToUpper', 'ToLowerInvariant' and 'ToUpperInvariant' to perform case-insensitive string comparisons, as in 'string.ToLower() == string.ToLower()', because they lead to an allocation. Instead, use 'string.Equals(string, StringComparison)' to perform case-insensitive comparisons. Switching to using an overload that takes a 'StringComparison' might cause subtle changes in behavior, so it's important to conduct thorough testing after applying the suggestion. Additionally, if a culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase'.</value>
20382038
</data>
20392039
<data name="RecommendCaseInsensitiveStringEqualsMessage" xml:space="preserve">
2040-
<value>Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison</value>
2040+
<value>Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase'</value>
20412041
</data>
20422042
<data name="RecommendCaseInsensitiveStringComparisonTitle" xml:space="preserve">
2043-
<value>Prefer the 'StringComparison' method overloads to perform case-insensitive string comparisons</value>
2043+
<value>Use the 'StringComparison' method overloads to perform case-insensitive string comparisons</value>
20442044
</data>
20452045
<data name="RecommendCaseInsensitiveStringComparisonDescription" xml:space="preserve">
2046-
<value>Avoid calling 'ToLower', 'ToUpper', 'ToLowerInvariant' and 'ToUpperInvariant' to perform case-insensitive string comparisons because they lead to an allocation. Instead, prefer calling the method overloads of 'Contains', 'IndexOf' and 'StartsWith' that take a 'StringComparison' enum value to perform case-insensitive comparisons.</value>
2046+
<value>Avoid calling 'ToLower', 'ToUpper', 'ToLowerInvariant' and 'ToUpperInvariant' to perform case-insensitive string comparisons because they lead to an allocation. Instead, prefer calling the method overloads of 'Contains', 'IndexOf' and 'StartsWith' that take a 'StringComparison' enum value to perform case-insensitive comparisons. Switching to using an overload that takes a 'StringComparison' might cause subtle changes in behavior, so it's important to conduct thorough testing after applying the suggestion. Additionally, if a culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase'.</value>
20472047
</data>
20482048
<data name="RecommendCaseInsensitiveStringComparisonMessage" xml:space="preserve">
2049-
<value>Prefer the string comparison method overload of '{0}' that takes a 'StringComparison' enum value to perform a case-insensitive comparison</value>
2049+
<value>Prefer the string comparison method overload of '{0}' that takes a 'StringComparison' enum value to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase'</value>
20502050
</data>
20512051
<data name="PreferDictionaryTryAddValueCodeFixTitle" xml:space="preserve">
20522052
<value>Use 'TryAdd(TKey, TValue)'</value>

0 commit comments

Comments
 (0)