Skip to content

Commit 51560f0

Browse files
committed
Updated new rule MiKo_5018 to simplify value comparisons for parameters and properties
(resolves #1090)
1 parent 4246153 commit 51560f0

File tree

3 files changed

+258
-13
lines changed

3 files changed

+258
-13
lines changed

MiKo.Analyzer.Shared/Rules/Performance/MiKo_5018_CodeFixProvider.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public sealed class MiKo_5018_CodeFixProvider : PerformanceCodeFixProvider
1414
{
1515
private static readonly SyntaxKind[] LogicalConditions = { SyntaxKind.LogicalAndExpression, SyntaxKind.LogicalOrExpression };
1616

17-
private static readonly SyntaxKind[] SpecialParentHandling = { SyntaxKind.ArrowExpressionClause, SyntaxKind.ReturnStatement, SyntaxKind.IfStatement };
17+
private static readonly SyntaxKind[] SpecialParents = { SyntaxKind.ArrowExpressionClause, SyntaxKind.ReturnStatement, SyntaxKind.IfStatement };
1818

1919
public override string FixableDiagnosticId => "MiKo_5018";
2020

@@ -25,11 +25,11 @@ protected override SyntaxNode GetUpdatedSyntax(Document document, SyntaxNode syn
2525
if (syntax is BinaryExpressionSyntax binary)
2626
{
2727
var left = FindProblematicNode(binary.Left); // TODO RKN: left will be "values.Length == 8"
28-
var right = binary.Right;
28+
var right = FindProblematicNode(binary.Right);
2929

3030
var updatedSyntax = binary.ReplaceNodes(new[] { left, right }, (original, rewritten) => ReferenceEquals(original, left) ? right.WithTriviaFrom(original) : left.WithTriviaFrom(original));
3131

32-
return binary.Parent.IsAnyKind(SpecialParentHandling)
32+
return binary.Parent.IsAnyKind(SpecialParents)
3333
? updatedSyntax.WithoutTrailingTrivia() // only remove trailing trivia if condition is direct child of 'if/return/arrow clause' so that semicolon fits
3434
: updatedSyntax;
3535
}
@@ -45,15 +45,18 @@ private static ExpressionSyntax FindProblematicNode(ExpressionSyntax expression)
4545
{
4646
if (node is BinaryExpressionSyntax binary && binary.IsAnyKind(LogicalConditions))
4747
{
48-
var right = binary.Right;
48+
var next = binary.Right;
4949

50-
if (right is BinaryExpressionSyntax binaryRight && (binaryRight.Left is ElementAccessExpressionSyntax || binaryRight.Right is ElementAccessExpressionSyntax))
50+
if (next is BinaryExpressionSyntax nested)
5151
{
52-
// we have some element access, so we need to replace the complete node
53-
return expression;
52+
if (nested.Left is ElementAccessExpressionSyntax || nested.Right is ElementAccessExpressionSyntax)
53+
{
54+
// we have some null checks or some element access, so we need to replace the complete node
55+
return expression;
56+
}
5457
}
5558

56-
node = right;
59+
node = next;
5760
}
5861
else
5962
{

MiKo.Analyzer.Shared/Rules/Performance/MiKo_5018_LogicalValueComparisonsComeFirstAnalyzer.cs

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,42 @@ private static bool IsNullCheck(ExpressionSyntax expression)
3535

3636
private static bool IsNullCheck(BinaryExpressionSyntax expression) => expression.Left.IsKind(SyntaxKind.NullLiteralExpression) || expression.Right.IsKind(SyntaxKind.NullLiteralExpression);
3737

38+
private static bool IsNumberCheck(ExpressionSyntax expression)
39+
{
40+
switch (expression.WithoutParenthesis())
41+
{
42+
case BinaryExpressionSyntax binary when IsNumberCheck(binary):
43+
return true;
44+
45+
default:
46+
return false;
47+
}
48+
}
49+
50+
private static bool IsNumberCheck(BinaryExpressionSyntax expression)
51+
{
52+
var left = expression.Left;
53+
var right = expression.Right;
54+
55+
if (left.IsKind(SyntaxKind.NumericLiteralExpression) || right.IsKind(SyntaxKind.NumericLiteralExpression))
56+
{
57+
return true;
58+
}
59+
60+
if (IsNumberCheck(left) && IsNumberCheck(right))
61+
{
62+
return true;
63+
}
64+
65+
return false;
66+
}
67+
3868
private static bool HasIssue(BinaryExpressionSyntax binary, SyntaxNodeAnalysisContext context)
3969
{
4070
if (binary.Right.WithoutParenthesis() is BinaryExpressionSyntax right && right.IsKind(SyntaxKind.EqualsExpression))
4171
{
72+
var semanticModel = context.SemanticModel;
73+
4274
if (binary.Left.WithoutParenthesis() is ExpressionSyntax left)
4375
{
4476
if (IsNullCheck(left))
@@ -47,18 +79,30 @@ private static bool HasIssue(BinaryExpressionSyntax binary, SyntaxNodeAnalysisCo
4779
return false;
4880
}
4981

82+
if (IsNumberCheck(left))
83+
{
84+
// do not report nested number checks
85+
return false;
86+
}
87+
5088
switch (left)
5189
{
5290
case IdentifierNameSyntax _: return false; // do not report checks on boolean members
5391
case IsPatternExpressionSyntax e when e.Pattern is DeclarationPatternSyntax: return false; // do not report pattern checks
92+
case MemberAccessExpressionSyntax m when m.GetTypeSymbol(semanticModel)?.IsValueType is true: return false; // do not report checks on value type members
5493
case BinaryExpressionSyntax nested:
5594
{
5695
if (nested.IsAnyKind(LogicalConditions))
5796
{
58-
if (IsNullCheck(nested.Right))
97+
if (IsNullCheck(nested.Right) && IsNullCheck(nested.Left))
5998
{
6099
return false; // do not report nested null checks
61100
}
101+
102+
if (IsNumberCheck(nested.Right) && IsNumberCheck(nested.Left))
103+
{
104+
return false; // do not report nested number checks
105+
}
62106
}
63107
else
64108
{
@@ -78,7 +122,7 @@ private static bool HasIssue(BinaryExpressionSyntax binary, SyntaxNodeAnalysisCo
78122
return false;
79123
}
80124

81-
if (nestedLeft.IsKind(SyntaxKind.NumericLiteralExpression) || nestedRight.IsKind(SyntaxKind.NumericLiteralExpression))
125+
if (IsNumberCheck(nested))
82126
{
83127
// do not report checks on value types
84128
return false;
@@ -89,6 +133,15 @@ private static bool HasIssue(BinaryExpressionSyntax binary, SyntaxNodeAnalysisCo
89133
// do not report on members
90134
return false;
91135
}
136+
137+
if (nestedLeft is IdentifierNameSyntax && nestedRight is IdentifierNameSyntax)
138+
{
139+
if (nestedLeft.GetTypeSymbol(semanticModel)?.IsValueType is true && nestedRight.GetTypeSymbol(semanticModel)?.IsValueType is true)
140+
{
141+
// do not report checks on value types
142+
return false;
143+
}
144+
}
92145
}
93146

94147
break;
@@ -102,8 +155,6 @@ private static bool HasIssue(BinaryExpressionSyntax binary, SyntaxNodeAnalysisCo
102155
return false;
103156
}
104157

105-
var semanticModel = context.SemanticModel;
106-
107158
if (right.Left.GetTypeSymbol(semanticModel)?.IsValueType is true && right.Right.GetTypeSymbol(semanticModel)?.IsValueType is true)
108159
{
109160
return true;

MiKo.Analyzer.Tests/Rules/Performance/MiKo_5018_LogicalValueComparisonsComeFirstAnalyzerTests.cs

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ public TestMe Nested { get }
7070
7171
public bool DoSomething(TestMe arg) => arg != null && arg.Nested != null && arg.Nested.Value == 42;
7272
}
73+
");
74+
75+
[Test]
76+
public void No_issue_is_reported_for_class_with_conditional_null_checks_and_value_type_comparison() => No_issue_is_reported_for(@"
77+
using System;
78+
79+
public class TestMe
80+
{
81+
public TestMe Nested { get }
82+
public int Value { get; }
83+
84+
public bool DoSomething(TestMe arg) => arg != null && arg.Nested?.Value == 42;
85+
}
7386
");
7487

7588
[Test]
@@ -234,6 +247,64 @@ public class TestMe
234247
235248
public bool SomeComparison(TestMe other) => other is null || (Value != other.Value && Data != other.Data);
236249
}
250+
");
251+
252+
[Test]
253+
public void No_issue_is_reported_for_complex_class_with_value_type_enum_OR_comparison() => No_issue_is_reported_for(@"
254+
using System;
255+
using System.Linq;
256+
257+
public class TestMe
258+
{
259+
public StringComparison Comparison { get; set; }
260+
261+
public Guid Id { get; set; }
262+
263+
public bool SomeComparison(TestMe other) => other.Id == Guid.Empty && (other.Comparison == StringComparison.Ordinal || other.Comparison == StringComparison.OrdinalIgnoreCase);
264+
}
265+
");
266+
267+
[Test]
268+
public void No_issue_is_reported_for_parameters_only_value_type_comparisons() => No_issue_is_reported_for(@"
269+
using System;
270+
271+
public class TestMe
272+
{
273+
public static bool SomeComparison(Guid guid1, Guid guid2, Guid guid3, Guid guid4) => guid1 == guid3 && guid2 == guid4;
274+
}
275+
");
276+
277+
[Test]
278+
public void No_issue_is_reported_for_CancellationToken_IsCancellationRequested_call() => No_issue_is_reported_for(@"
279+
using System;
280+
using System.Threading;
281+
282+
public class TestMe
283+
{
284+
public static bool SomeComparison(CancellationToken token, int value) => token.IsCancellationRequested || value == -1;
285+
}
286+
");
287+
288+
[Test]
289+
public void No_issue_is_reported_for_version_comparison() => No_issue_is_reported_for(@"
290+
using System;
291+
using System.Threading;
292+
293+
public class TestMe
294+
{
295+
public static bool SomeComparison(Version version) => version.Major == 0 && version.Minor == 0 && version.Build == 0 && version.Revision == 0;
296+
}
297+
");
298+
299+
[Test]
300+
public void No_issue_is_reported_for_version_comparison_when_numbers_are_on_left_side() => No_issue_is_reported_for(@"
301+
using System;
302+
using System.Threading;
303+
304+
public class TestMe
305+
{
306+
public static bool SomeComparison(Version version) => 0 == version.Major && 0 == version.Minor && 0 == version.Build && 0 == version.Revision;
307+
}
237308
");
238309

239310
[Test]
@@ -339,6 +410,32 @@ public class TestMe
339410
{
340411
public bool DoSomething(int i, object o) => o.ToString() == ""whatever"" || i == 42;
341412
}
413+
");
414+
415+
[Test]
416+
public void An_issue_is_reported_for_class_with_reference_type_comparison_and_multiple_null_checks_and_value_type_comparison() => An_issue_is_reported_for(@"
417+
using System;
418+
419+
public class TestMe
420+
{
421+
public TestMe Nested { get }
422+
public int Value { get; }
423+
424+
public bool DoSomething(TestMe arg, object o) => o.ToString() == ""whatever"" && arg != null && arg.Nested != null && arg.Nested.Value == 42;
425+
}
426+
");
427+
428+
[Test]
429+
public void An_issue_is_reported_for_class_with_conditional_value_type_comparison_and_reference_type_comparison_if_string_invocation_comes_first() => An_issue_is_reported_for(@"
430+
using System;
431+
432+
public class TestMe
433+
{
434+
public TestMe Nested { get }
435+
public int Value { get; }
436+
437+
public bool DoSomething(TestMe arg, object o) => o.ToString() == ""whatever"" && arg?.Nested?.Value == 42;
438+
}
342439
");
343440

344441
[Test]
@@ -651,7 +748,101 @@ public bool DoSomething(int i, int[] values)
651748
VerifyCSharpFix(OriginalText, FixedText);
652749
}
653750

654-
//// TODO RKN: Add more complex AND/OR Comparisons with at least 3 comparisons and parenthesized ones
751+
[Test]
752+
public void Code_gets_fixed_for_class_with_reference_type_comparison_and_multiple_null_checks_and_value_type_comparison()
753+
{
754+
const string OriginalCode = @"
755+
using System;
756+
757+
public class TestMe
758+
{
759+
public TestMe Nested { get }
760+
public int Value { get; }
761+
762+
public bool DoSomething(TestMe arg, object o) => o.ToString() == ""whatever"" && arg != null && arg.Nested != null && arg.Nested.Value == 42;
763+
}
764+
";
765+
766+
const string FixedCode = @"
767+
using System;
768+
769+
public class TestMe
770+
{
771+
public TestMe Nested { get }
772+
public int Value { get; }
773+
774+
public bool DoSomething(TestMe arg, object o) => arg != null && arg.Nested != null && arg.Nested.Value == 42 && o.ToString() == ""whatever"";
775+
}
776+
";
777+
778+
VerifyCSharpFix(OriginalCode, FixedCode);
779+
}
780+
781+
[Test]
782+
public void Code_gets_fixed_for_class_with_conditional_value_type_comparison_and_reference_type_comparison_if_string_invocation_comes_first()
783+
{
784+
const string OriginalCode = @"
785+
using System;
786+
787+
public class TestMe
788+
{
789+
public TestMe Nested { get }
790+
public int Value { get; }
791+
792+
public bool DoSomething(TestMe arg, object o) => o.ToString() == ""whatever"" && arg?.Nested?.Value == 42;
793+
}
794+
";
795+
796+
const string FixedCode = @"
797+
using System;
798+
799+
public class TestMe
800+
{
801+
public TestMe Nested { get }
802+
public int Value { get; }
803+
804+
public bool DoSomething(TestMe arg, object o) => arg?.Nested?.Value == 42 && o.ToString() == ""whatever"";
805+
}
806+
";
807+
808+
VerifyCSharpFix(OriginalCode, FixedCode);
809+
}
810+
811+
[Test]
812+
public void Code_gets_fixed_for_class_with_conditional_value_type_comparison_and_boolean_method_calls_if_method_calls_come_first()
813+
{
814+
const string OriginalCode = @"
815+
using System;
816+
817+
public class TestMe
818+
{
819+
public StringComparison Comparison { get }
820+
821+
public bool DoSomething(TestMe t) => IsSomething(t) && FindSomething(t) == t && t.Comparison == StringComparison.Ordinal;
822+
823+
public bool IsSomething(object o) => true;
824+
825+
public object FindSomething(object o) => o;
826+
}
827+
";
828+
829+
const string FixedCode = @"
830+
using System;
831+
832+
public class TestMe
833+
{
834+
public StringComparison Comparison { get }
835+
836+
public bool DoSomething(TestMe t) => t.Comparison == StringComparison.Ordinal && IsSomething(t) && FindSomething(t) == t;
837+
838+
public bool IsSomething(object o) => true;
839+
840+
public object FindSomething(object o) => o;
841+
}
842+
";
843+
844+
VerifyCSharpFix(OriginalCode, FixedCode);
845+
}
655846

656847
protected override string GetDiagnosticId() => MiKo_5018_LogicalValueComparisonsComeFirstAnalyzer.Id;
657848

0 commit comments

Comments
 (0)