Skip to content

Commit f982601

Browse files
authored
added numeric & comparable analyzers (#28)
1 parent e7f0cb6 commit f982601

11 files changed

+436
-17
lines changed

src/FluentAssertions.Analyzers.Tests/GenerateCode.cs

+29-14
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,16 @@ public static string EnumerableExpressionBodyAssertion(string assertion) => Enum
2929
.AppendLine(" public bool BooleanProperty { get; set; }")
3030
.AppendLine(" public string Message { get; set; }")
3131
.AppendLine(" }")
32-
.AppendLine(" class Program")
33-
.AppendLine(" {")
34-
.AppendLine(" public static void Main()")
35-
.AppendLine(" {")
36-
.AppendLine(" }")
37-
.AppendLine(" }")
32+
.AppendMainMethod()
3833
.AppendLine("}")
3934
.ToString();
4035

4136
public static string DictionaryAssertion(string assertion) => new StringBuilder()
4237
.AppendLine("using System.Collections.Generic;")
4338
.AppendLine("using System.Linq;")
4439
.AppendLine("using System;")
45-
.AppendLine("using FluentAssertions;using FluentAssertions.Extensions;")
40+
.AppendLine("using FluentAssertions;")
41+
.AppendLine("using FluentAssertions.Extensions;")
4642
.AppendLine("namespace TestNamespace")
4743
.AppendLine("{")
4844
.AppendLine(" public class TestClass")
@@ -56,25 +52,41 @@ public static string EnumerableExpressionBodyAssertion(string assertion) => Enum
5652
.AppendLine(" {")
5753
.AppendLine(" public bool BooleanProperty { get; set; }")
5854
.AppendLine(" }")
59-
.AppendLine(" class Program")
55+
.AppendMainMethod()
56+
.AppendLine("}")
57+
.ToString();
58+
59+
public static string NumericAssertion(string assertion) => new StringBuilder()
60+
.AppendLine("using System;")
61+
.AppendLine("using FluentAssertions;")
62+
.AppendLine("using FluentAssertions.Extensions;")
63+
.AppendLine("namespace TestNamespace")
64+
.AppendLine("{")
65+
.AppendLine(" class TestClass")
6066
.AppendLine(" {")
61-
.AppendLine(" public static void Main()")
67+
.AppendLine(" void TestMethod(double actual, double expected, double lower, double upper, double delta)")
6268
.AppendLine(" {")
69+
.AppendLine($" {assertion}")
6370
.AppendLine(" }")
6471
.AppendLine(" }")
72+
.AppendMainMethod()
6573
.AppendLine("}")
6674
.ToString();
6775

68-
public static string NumericAssertion(string assertion) => new StringBuilder()
76+
public static string ComparableAssertion(string assertion) => new StringBuilder()
77+
.AppendLine("using System;")
78+
.AppendLine("using FluentAssertions;")
79+
.AppendLine("using FluentAssertions.Extensions;")
6980
.AppendLine("namespace TestNamespace")
7081
.AppendLine("{")
7182
.AppendLine(" class TestClass")
7283
.AppendLine(" {")
73-
.AppendLine(" void TestMethod(int actual, int expected)")
84+
.AppendLine(" void TestMethod(IComparable<int> actual, int expected)")
7485
.AppendLine(" {")
7586
.AppendLine($" {assertion}")
7687
.AppendLine(" }")
7788
.AppendLine(" }")
89+
.AppendMainMethod()
7890
.AppendLine("}")
7991
.ToString();
8092

@@ -90,13 +102,16 @@ public static string EnumerableExpressionBodyAssertion(string assertion) => Enum
90102
.AppendLine($" {assertion}")
91103
.AppendLine(" }")
92104
.AppendLine(" }")
105+
.AppendMainMethod()
106+
.AppendLine("}")
107+
.ToString();
108+
109+
private static StringBuilder AppendMainMethod(this StringBuilder builder) => builder
93110
.AppendLine(" class Program")
94111
.AppendLine(" {")
95112
.AppendLine(" public static void Main()")
96113
.AppendLine(" {")
97114
.AppendLine(" }")
98-
.AppendLine(" }")
99-
.AppendLine("}")
100-
.ToString();
115+
.AppendLine(" }");
101116
}
102117
}

src/FluentAssertions.Analyzers.Tests/Tips/DictionaryTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ public class DictionaryTests
164164
Message = message,
165165
Locations = new DiagnosticResultLocation[]
166166
{
167-
new DiagnosticResultLocation("Test0.cs", 11,13)
167+
new DiagnosticResultLocation("Test0.cs", 12,13)
168168
},
169169
Severity = DiagnosticSeverity.Info
170170
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
4+
namespace FluentAssertions.Analyzers.Tests.Tips
5+
{
6+
[TestClass]
7+
public class NumericTests
8+
{
9+
[AssertionDataTestMethod]
10+
[AssertionDiagnostic("actual.Should().BeGreaterThan(0{0});")]
11+
[AssertionDiagnostic("actual.Should().BeGreaterThan(0{0}).ToString();")]
12+
[Implemented]
13+
public void NumericShouldBePositive_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic<NumericShouldBePositiveAnalyzer>(assertion);
14+
15+
[AssertionDataTestMethod]
16+
[AssertionCodeFix(
17+
oldAssertion: "actual.Should().BeGreaterThan(0{0});",
18+
newAssertion: "actual.Should().BePositive({0});")]
19+
[AssertionCodeFix(
20+
oldAssertion: "actual.Should().BeGreaterThan(0{0}).ToString();",
21+
newAssertion: "actual.Should().BePositive({0}).ToString();")]
22+
[Implemented]
23+
public void NumericShouldBePositive_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix<NumericShouldBePositiveCodeFix, NumericShouldBePositiveAnalyzer>(oldAssertion, newAssertion);
24+
25+
[AssertionDataTestMethod]
26+
[AssertionDiagnostic("actual.Should().BeLessThan(0{0});")]
27+
[AssertionDiagnostic("actual.Should().BeLessThan(0{0}).ToString();")]
28+
[Implemented]
29+
public void NumericShouldBeNegative_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic<NumericShouldBeNegativeAnalyzer>(assertion);
30+
31+
[AssertionDataTestMethod]
32+
[AssertionCodeFix(
33+
oldAssertion: "actual.Should().BeLessThan(0{0});",
34+
newAssertion: "actual.Should().BeNegative({0});")]
35+
[AssertionCodeFix(
36+
oldAssertion: "actual.Should().BeLessThan(0{0}).ToString();",
37+
newAssertion: "actual.Should().BeNegative({0}).ToString();")]
38+
[Implemented]
39+
public void NumericShouldBeNegative_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix<NumericShouldBeNegativeCodeFix, NumericShouldBeNegativeAnalyzer>(oldAssertion, newAssertion);
40+
41+
[AssertionDataTestMethod]
42+
[AssertionDiagnostic("actual.Should().BeGreaterOrEqualTo(lower{0}).And.BeLessOrEqualTo(upper);")]
43+
[AssertionDiagnostic("actual.Should().BeGreaterOrEqualTo(lower).And.BeLessOrEqualTo(upper{0});")]
44+
[AssertionDiagnostic("actual.Should().BeGreaterOrEqualTo(lower{0}).And.BeLessOrEqualTo(upper{0});")]
45+
[AssertionDiagnostic("actual.Should().BeLessOrEqualTo(upper{0}).And.BeGreaterOrEqualTo(lower);")]
46+
[AssertionDiagnostic("actual.Should().BeLessOrEqualTo(upper).And.BeGreaterOrEqualTo(lower{0});")]
47+
[AssertionDiagnostic("actual.Should().BeLessOrEqualTo(upper{0}).And.BeGreaterOrEqualTo(lower{0});")]
48+
[Implemented]
49+
public void NumericShouldBeInRange_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic<NumericShouldBeInRangeAnalyzer>(assertion);
50+
51+
[AssertionDataTestMethod]
52+
[AssertionCodeFix(
53+
oldAssertion: "actual.Should().BeGreaterOrEqualTo(lower{0}).And.BeLessOrEqualTo(upper);",
54+
newAssertion: "actual.Should().BeInRange(lower, upper{0});")]
55+
[AssertionCodeFix(
56+
oldAssertion: "actual.Should().BeGreaterOrEqualTo(lower).And.BeLessOrEqualTo(upper{0});",
57+
newAssertion: "actual.Should().BeInRange(lower, upper{0});")]
58+
[AssertionCodeFix(
59+
oldAssertion: "actual.Should().BeLessOrEqualTo(upper{0}).And.BeGreaterOrEqualTo(lower);",
60+
newAssertion: "actual.Should().BeInRange(lower, upper{0});")]
61+
[AssertionCodeFix(
62+
oldAssertion: "actual.Should().BeLessOrEqualTo(upper).And.BeGreaterOrEqualTo(lower{0});",
63+
newAssertion: "actual.Should().BeInRange(lower, upper{0});")]
64+
[NotImplemented]
65+
public void NumericShouldBeInRange_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix<NumericShouldBeApproximatelyCodeFix, NumericShouldBeInRangeAnalyzer>(oldAssertion, newAssertion);
66+
67+
[AssertionDataTestMethod]
68+
[AssertionDiagnostic("Math.Abs(expected - actual).Should().BeLessOrEqualTo(delta{0});")]
69+
[Implemented]
70+
public void NumericShouldBeApproximately_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic<NumericShouldBeApproximatelyAnalyzer>(assertion);
71+
72+
[AssertionDataTestMethod]
73+
[AssertionCodeFix(
74+
oldAssertion: "Math.Abs(expected - actual).Should().BeLessOrEqualTo(delta{0});",
75+
newAssertion: "actual.Should().BeApproximately(expected, delta{0});")]
76+
[Implemented]
77+
public void NumericShouldBeApproximately_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix<NumericShouldBeApproximatelyCodeFix, NumericShouldBeApproximatelyAnalyzer>(oldAssertion, newAssertion);
78+
79+
private void VerifyCSharpDiagnostic<TDiagnosticAnalyzer>(string sourceAssertion) where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new()
80+
{
81+
var source = GenerateCode.NumericAssertion(sourceAssertion);
82+
83+
var type = typeof(TDiagnosticAnalyzer);
84+
var diagnosticId = (string)type.GetField("DiagnosticId").GetValue(null);
85+
var message = (string)type.GetField("Message").GetValue(null);
86+
87+
DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source, new DiagnosticResult
88+
{
89+
Id = diagnosticId,
90+
Message = message,
91+
Locations = new DiagnosticResultLocation[]
92+
{
93+
new DiagnosticResultLocation("Test0.cs", 10, 13)
94+
},
95+
Severity = DiagnosticSeverity.Info
96+
});
97+
}
98+
99+
private void VerifyCSharpFix<TCodeFixProvider, TDiagnosticAnalyzer>(string oldSourceAssertion, string newSourceAssertion)
100+
where TCodeFixProvider : Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider, new()
101+
where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new()
102+
{
103+
var oldSource = GenerateCode.NumericAssertion(oldSourceAssertion);
104+
var newSource = GenerateCode.NumericAssertion(newSourceAssertion);
105+
106+
DiagnosticVerifier.VerifyCSharpFix<TCodeFixProvider, TDiagnosticAnalyzer>(oldSource, newSource);
107+
}
108+
}
109+
}

src/FluentAssertions.Analyzers/Constants.cs

+16
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public static class Collections
4949
public const string CollectionShouldOnlyHaveUniqueItemsByComparer = nameof(CollectionShouldOnlyHaveUniqueItemsByComparer);
5050
public const string CollectionShouldHaveElementAt0Null = nameof(CollectionShouldHaveElementAt0Null);
5151
}
52+
5253
public static class Dictionaries
5354
{
5455
public const string DictionaryShouldContainKey = nameof(DictionaryShouldContainKey);
@@ -58,6 +59,7 @@ public static class Dictionaries
5859
public const string DictionaryShouldNotContainKey = nameof(DictionaryShouldNotContainKey);
5960
public const string DictionaryShouldNotContainValue = nameof(DictionaryShouldNotContainValue);
6061
}
62+
6163
public static class Strings
6264
{
6365
public const string StringShouldStartWith = nameof(StringShouldStartWith);
@@ -68,7 +70,21 @@ public static class Strings
6870
public const string StringShouldNotBeNullOrWhiteSpace = nameof(StringShouldNotBeNullOrWhiteSpace);
6971
public const string StringShouldHaveLength = nameof(StringShouldHaveLength);
7072
}
73+
74+
public static class Comparable
75+
{
76+
public const string ComparableShouldBePositive = nameof(ComparableShouldBePositive);
77+
}
78+
79+
public static class Numeric
80+
{
81+
public const string NumericShouldBePositive = nameof(NumericShouldBePositive);
82+
public const string NumericShouldBeNegative = nameof(NumericShouldBeNegative);
83+
public const string NumericShouldBeInRange = nameof(NumericShouldBeInRange);
84+
public const string NumericShouldBeApproximately = nameof(NumericShouldBeApproximately);
85+
}
7186
}
87+
7288
public static class CodeSmell
7389
{
7490
public const string Category = "FluentAssertionCodeSmell";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Microsoft.CodeAnalysis;
2+
3+
namespace FluentAssertions.Analyzers
4+
{
5+
public abstract class NumericAnalyzer : FluentAssertionsAnalyzer
6+
{
7+
protected override bool ShouldAnalyzeVariableType(TypeInfo typeInfo)
8+
{
9+
return true;
10+
}
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CodeFixes;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Collections.Immutable;
9+
using System.Composition;
10+
using System.Linq;
11+
12+
namespace FluentAssertions.Analyzers
13+
{
14+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
15+
public class NumericShouldBeApproximatelyAnalyzer : NumericAnalyzer
16+
{
17+
public const string DiagnosticId = Constants.Tips.Numeric.NumericShouldBeApproximately;
18+
public const string Category = Constants.Tips.Category;
19+
20+
public const string Message = "Use .Should() followed by .BeApproximately() instead.";
21+
22+
protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true);
23+
protected override IEnumerable<FluentAssertionsCSharpSyntaxVisitor> Visitors
24+
{
25+
get
26+
{
27+
yield return new MathAbsShouldBeLessOrEqualToSyntaxVisitor();
28+
}
29+
}
30+
31+
private static readonly string[] ValidaTypeNames = { "double", "decimal", "float" };
32+
protected override bool ShouldAnalyzeVariableType(TypeInfo typeInfo)
33+
{
34+
return ValidaTypeNames.Contains(typeInfo.ConvertedType.ToDisplayString(), StringComparer.OrdinalIgnoreCase);
35+
}
36+
37+
public class MathAbsShouldBeLessOrEqualToSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor
38+
{
39+
public MathAbsShouldBeLessOrEqualToSyntaxVisitor() : base(new MemberValidator("Abs", IsSubtractExpression), MemberValidator.Should, new MemberValidator("BeLessOrEqualTo"))
40+
{
41+
}
42+
43+
private static bool IsSubtractExpression(SeparatedSyntaxList<ArgumentSyntax> arguments)
44+
{
45+
if (arguments.Count != 1) return false;
46+
47+
return arguments[0].Expression is BinaryExpressionSyntax subtractExpression
48+
&& subtractExpression.IsKind(SyntaxKind.SubtractExpression)
49+
&& subtractExpression.Right.IsKind(SyntaxKind.IdentifierName);
50+
}
51+
}
52+
}
53+
54+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(NumericShouldBeApproximatelyCodeFix)), Shared]
55+
public class NumericShouldBeApproximatelyCodeFix : FluentAssertionsCodeFixProvider
56+
{
57+
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(NumericShouldBeApproximatelyAnalyzer.DiagnosticId);
58+
59+
protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)
60+
{
61+
var remove = NodeReplacement.RemoveAndExtractArguments("Abs");
62+
var newExpression = GetNewExpression(expression, remove);
63+
64+
var subtractExpression = (BinaryExpressionSyntax)remove.Arguments[0].Expression;
65+
66+
var actual = subtractExpression.Right as IdentifierNameSyntax;
67+
var expected = subtractExpression.Left;
68+
69+
newExpression = GetNewExpression(newExpression, NodeReplacement.RenameAndPrependArguments("BeLessOrEqualTo", "BeApproximately", new SeparatedSyntaxList<ArgumentSyntax>().Add(SyntaxFactory.Argument(expected))));
70+
71+
newExpression = RenameIdentifier(newExpression, "Math", actual.Identifier.Text);
72+
73+
return newExpression;
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)