Skip to content

Commit d66d671

Browse files
authored
add should.equals (#95)
* add should.equals * add condition for applying code fix for Equals * fix tests * use single codeFix for all scenarios * oops
1 parent e16dfce commit d66d671

File tree

9 files changed

+213
-19
lines changed

9 files changed

+213
-19
lines changed

src/FluentAssertions.Analyzers.Tests/GenerateCode.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,23 @@ public static string EnumerableExpressionBodyAssertion(string assertion) => Enum
140140
.AppendLine("}")
141141
.ToString();
142142

143+
public static string ObjectStatement(string statement) => new StringBuilder()
144+
.AppendLine("using System;")
145+
.AppendLine("using System.Threading.Tasks;")
146+
.AppendLine("using FluentAssertions;using FluentAssertions.Extensions;")
147+
.AppendLine("namespace TestNamespace")
148+
.AppendLine("{")
149+
.AppendLine(" class TestClass")
150+
.AppendLine(" {")
151+
.AppendLine(" void TestMethod(object actual, object expected)")
152+
.AppendLine(" {")
153+
.AppendLine($" {statement}")
154+
.AppendLine(" }")
155+
.AppendLine(" }")
156+
.AppendMainMethod()
157+
.AppendLine("}")
158+
.ToString();
159+
143160
private static StringBuilder AppendMainMethod(this StringBuilder builder) => builder
144161
.AppendLine(" class Program")
145162
.AppendLine(" {")
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using FluentAssertions.Analyzers.Tips;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
5+
namespace FluentAssertions.Analyzers.Tests.Tips
6+
{
7+
[TestClass]
8+
public class ShouldEqualsTests
9+
{
10+
[TestMethod]
11+
[Implemented]
12+
public void ShouldEquals_TestAnalyzer()
13+
=> VerifyCSharpDiagnosticExpressionBody("actual.Should().Equals(expected);");
14+
15+
[TestMethod]
16+
[Implemented]
17+
public void ShouldEquals_ShouldBe_ObjectType_TestCodeFix()
18+
{
19+
var oldSource = GenerateCode.ObjectStatement("actual.Should().Equals(expected);");
20+
var newSource = GenerateCode.ObjectStatement("actual.Should().Be(expected);");
21+
22+
DiagnosticVerifier.VerifyCSharpFix<ShouldEqualsCodeFix, ShouldEqualsAnalyzer>(oldSource, newSource);
23+
}
24+
25+
[TestMethod]
26+
[Implemented]
27+
public void ShouldEquals_ShouldBe_NumberType_TestCodeFix()
28+
{
29+
var oldSource = GenerateCode.NumericAssertion("actual.Should().Equals(expected);");
30+
var newSource = GenerateCode.NumericAssertion("actual.Should().Be(expected);");
31+
32+
DiagnosticVerifier.VerifyCSharpFix<ShouldEqualsCodeFix, ShouldEqualsAnalyzer>(oldSource, newSource);
33+
}
34+
35+
[TestMethod]
36+
[Implemented]
37+
public void ShouldEquals_ShouldBe_StringType_TestCodeFix()
38+
{
39+
var oldSource = GenerateCode.StringAssertion("actual.Should().Equals(expected);");
40+
var newSource = GenerateCode.StringAssertion("actual.Should().Be(expected);");
41+
42+
DiagnosticVerifier.VerifyCSharpFix<ShouldEqualsCodeFix, ShouldEqualsAnalyzer>(oldSource, newSource);
43+
}
44+
45+
[TestMethod]
46+
[Implemented]
47+
public void ShouldEquals_ShouldEqual_EnumerableType_TestCodeFix()
48+
{
49+
var oldSource = GenerateCode.EnumerableCodeBlockAssertion("actual.Should().Equals(expected);");
50+
var newSource = GenerateCode.EnumerableCodeBlockAssertion("actual.Should().Equal(expected);");
51+
52+
DiagnosticVerifier.VerifyCSharpFix<ShouldEqualsCodeFix, ShouldEqualsAnalyzer>(oldSource, newSource);
53+
}
54+
55+
private void VerifyCSharpDiagnosticExpressionBody(string sourceAssertion)
56+
{
57+
var source = GenerateCode.ObjectStatement(sourceAssertion);
58+
DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source, new DiagnosticResult
59+
{
60+
Id = ShouldEqualsAnalyzer.DiagnosticId,
61+
Message = ShouldEqualsAnalyzer.Message,
62+
Locations = new DiagnosticResultLocation[]
63+
{
64+
new DiagnosticResultLocation("Test0.cs", 10,13)
65+
},
66+
Severity = DiagnosticSeverity.Info
67+
});
68+
}
69+
}
70+
}

src/FluentAssertions.Analyzers/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ public static class CodeSmell
9191

9292
public const string NullConditionalAssertion = nameof(NullConditionalAssertion);
9393
public const string AsyncVoid = nameof(AsyncVoid);
94+
public const string ShouldEquals = nameof(ShouldEquals);
9495
}
9596
}
9697
}

src/FluentAssertions.Analyzers/Tips/Collections/CollectionShouldNotBeNullOrEmpty.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Collections.Immutable;
77
using System.Composition;
8+
using System.Threading.Tasks;
89

910
namespace FluentAssertions.Analyzers
1011
{
@@ -46,16 +47,18 @@ public class CollectionShouldNotBeNullOrEmptyCodeFix : FluentAssertionsCodeFixPr
4647
{
4748
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(CollectionShouldNotBeNullOrEmptyAnalyzer.DiagnosticId);
4849

49-
protected override bool CanRewriteAssertion(ExpressionSyntax expression)
50+
protected override Task<bool> CanRewriteAssertion(ExpressionSyntax expression, CodeFixContext context)
5051
{
5152
var visitor = new MemberAccessExpressionsCSharpSyntaxVisitor();
5253
expression.Accept(visitor);
5354

5455
var notBeEmpty = visitor.Members.Find(member => member.Name.Identifier.Text == "NotBeEmpty");
5556
var notBeNull = visitor.Members.Find(member => member.Name.Identifier.Text == "NotBeNull");
5657

57-
return !(notBeEmpty.Parent is InvocationExpressionSyntax notBeEmptyInvocation && notBeEmptyInvocation.ArgumentList.Arguments.Any()
58-
&& notBeNull.Parent is InvocationExpressionSyntax notBeNullInvocation && notBeNullInvocation.ArgumentList.Arguments.Any());
58+
return Task.FromResult(
59+
!(notBeEmpty.Parent is InvocationExpressionSyntax notBeEmptyInvocation && notBeEmptyInvocation.ArgumentList.Arguments.Any()
60+
&& notBeNull.Parent is InvocationExpressionSyntax notBeNullInvocation && notBeNullInvocation.ArgumentList.Arguments.Any())
61+
);
5962
}
6063

6164
protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)

src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainKeyAndValue.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.Collections.Immutable;
88
using System.Composition;
9+
using System.Threading.Tasks;
910

1011
namespace FluentAssertions.Analyzers
1112
{
@@ -48,16 +49,18 @@ public class DictionaryShouldContainKeyAndValueCodeFix : FluentAssertionsCodeFix
4849
{
4950
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(DictionaryShouldContainKeyAndValueAnalyzer.DiagnosticId);
5051

51-
protected override bool CanRewriteAssertion(ExpressionSyntax expression)
52+
protected override Task<bool> CanRewriteAssertion(ExpressionSyntax expression, CodeFixContext context)
5253
{
5354
var visitor = new MemberAccessExpressionsCSharpSyntaxVisitor();
5455
expression.Accept(visitor);
5556

5657
var containKey = visitor.Members.Find(member => member.Name.Identifier.Text == "ContainKey");
5758
var containValue = visitor.Members.Find(member => member.Name.Identifier.Text == "ContainValue");
5859

59-
return !(containKey.Parent is InvocationExpressionSyntax containKeyInvocation && containKeyInvocation.ArgumentList.Arguments.Count > 1
60-
&& containValue.Parent is InvocationExpressionSyntax containValueInvocation && containValueInvocation.ArgumentList.Arguments.Count > 1);
60+
return Task.FromResult(
61+
!(containKey.Parent is InvocationExpressionSyntax containKeyInvocation && containKeyInvocation.ArgumentList.Arguments.Count > 1
62+
&& containValue.Parent is InvocationExpressionSyntax containValueInvocation && containValueInvocation.ArgumentList.Arguments.Count > 1)
63+
);
6164
}
6265

6366
protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)

src/FluentAssertions.Analyzers/Tips/Dictionaries/DictionaryShouldContainPair.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.Collections.Immutable;
88
using System.Composition;
9+
using System.Threading.Tasks;
910

1011
namespace FluentAssertions.Analyzers
1112
{
@@ -96,16 +97,18 @@ public class DictionaryShouldContainPairCodeFix : FluentAssertionsCodeFixProvide
9697
{
9798
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(DictionaryShouldContainPairAnalyzer.DiagnosticId);
9899

99-
protected override bool CanRewriteAssertion(ExpressionSyntax expression)
100+
protected override Task<bool> CanRewriteAssertion(ExpressionSyntax expression, CodeFixContext context)
100101
{
101102
var visitor = new MemberAccessExpressionsCSharpSyntaxVisitor();
102103
expression.Accept(visitor);
103104

104105
var containKey = visitor.Members.Find(member => member.Name.Identifier.Text == "ContainKey");
105106
var containValue = visitor.Members.Find(member => member.Name.Identifier.Text == "ContainValue");
106107

107-
return !(containKey.Parent is InvocationExpressionSyntax containKeyInvocation && containKeyInvocation.ArgumentList.Arguments.Count > 1
108-
&& containValue.Parent is InvocationExpressionSyntax containValueInvocation && containValueInvocation.ArgumentList.Arguments.Count > 1);
108+
return Task.FromResult(
109+
!(containKey.Parent is InvocationExpressionSyntax containKeyInvocation && containKeyInvocation.ArgumentList.Arguments.Count > 1
110+
&& containValue.Parent is InvocationExpressionSyntax containValueInvocation && containValueInvocation.ArgumentList.Arguments.Count > 1)
111+
);
109112
}
110113

111114
protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)

src/FluentAssertions.Analyzers/Tips/Numerics/NumericShouldBeInRange.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.Collections.Immutable;
88
using System.Composition;
9+
using System.Threading.Tasks;
910

1011
namespace FluentAssertions.Analyzers
1112
{
@@ -46,16 +47,18 @@ public class NumericShouldBeInRangeCodeFix : FluentAssertionsCodeFixProvider
4647
{
4748
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(NumericShouldBeInRangeAnalyzer.DiagnosticId);
4849

49-
protected override bool CanRewriteAssertion(ExpressionSyntax expression)
50+
protected override Task<bool> CanRewriteAssertion(ExpressionSyntax expression, CodeFixContext context)
5051
{
5152
var visitor = new MemberAccessExpressionsCSharpSyntaxVisitor();
5253
expression.Accept(visitor);
5354

5455
var beLessOrEqualTo = visitor.Members.Find(member => member.Name.Identifier.Text == "BeLessOrEqualTo");
5556
var beGreaterOrEqualTo = visitor.Members.Find(member => member.Name.Identifier.Text == "BeGreaterOrEqualTo");
5657

57-
return !(beLessOrEqualTo.Parent is InvocationExpressionSyntax beLessOrEqualToInvocation && beLessOrEqualToInvocation.ArgumentList.Arguments.Count > 1
58-
&& beGreaterOrEqualTo.Parent is InvocationExpressionSyntax beGreaterOrEqualToInvocation && beGreaterOrEqualToInvocation.ArgumentList.Arguments.Count > 1);
58+
return Task.FromResult(
59+
!(beLessOrEqualTo.Parent is InvocationExpressionSyntax beLessOrEqualToInvocation && beLessOrEqualToInvocation.ArgumentList.Arguments.Count > 1
60+
&& beGreaterOrEqualTo.Parent is InvocationExpressionSyntax beGreaterOrEqualToInvocation && beGreaterOrEqualToInvocation.ArgumentList.Arguments.Count > 1)
61+
);
5962
}
6063

6164
protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CodeFixes;
3+
using Microsoft.CodeAnalysis.CSharp.Syntax;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using System.Collections;
6+
using System.Collections.Generic;
7+
using System.Collections.Immutable;
8+
using System.Composition;
9+
using System.IO;
10+
using System.Linq;
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
14+
namespace FluentAssertions.Analyzers.Tips
15+
{
16+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
17+
public class ShouldEqualsAnalyzer : FluentAssertionsAnalyzer
18+
{
19+
public const string DiagnosticId = Constants.CodeSmell.ShouldEquals;
20+
public const string Category = Constants.CodeSmell.Category;
21+
public const string Message = ".Should().Equals() is not an assertion, it just calls the native Object.Equals method.\n"
22+
+ "Use .Should().Equal() for collections or .Should().Be() for other types";
23+
24+
protected override DiagnosticDescriptor Rule => new DiagnosticDescriptor(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true);
25+
protected override IEnumerable<FluentAssertionsCSharpSyntaxVisitor> Visitors
26+
{
27+
get
28+
{
29+
yield return new ShouldEqualsSyntaxVisitor();
30+
}
31+
}
32+
33+
public class ShouldEqualsSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor
34+
{
35+
public ShouldEqualsSyntaxVisitor() : base(MemberValidator.Should, new MemberValidator("Equals"))
36+
{
37+
}
38+
}
39+
}
40+
41+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ShouldEqualsCodeFix)), Shared]
42+
public class ShouldEqualsCodeFix : FluentAssertionsCodeFixProvider
43+
{
44+
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(ShouldEqualsAnalyzer.DiagnosticId);
45+
46+
protected override async Task<bool> CanRewriteAssertion(ExpressionSyntax expression, CodeFixContext context)
47+
{
48+
var model = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
49+
50+
var visitor = new MemberAccessExpressionsCSharpSyntaxVisitor();
51+
expression.Accept(visitor);
52+
53+
var member = visitor.Members[visitor.Members.Count - 1];
54+
var info = model.GetTypeInfo(member.Expression);
55+
56+
var taskCompletionSourceInfo = model.Compilation.GetTypeByMetadataName(typeof(TaskCompletionSource<>).FullName);
57+
if (info.Type.Equals(taskCompletionSourceInfo)) return false;
58+
59+
var streamInfo = model.Compilation.GetTypeByMetadataName(typeof(Stream).FullName);
60+
if (info.Type.AllInterfaces.Contains(streamInfo)) return false;
61+
62+
return true;
63+
}
64+
65+
protected override async Task<ExpressionSyntax> GetNewExpressionAsync(ExpressionSyntax expression, Document document, FluentAssertionsDiagnosticProperties properties, CancellationToken cancellationToken)
66+
{
67+
var model = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
68+
69+
var visitor = new MemberAccessExpressionsCSharpSyntaxVisitor();
70+
expression.Accept(visitor);
71+
72+
var member = visitor.Members[visitor.Members.Count - 1];
73+
var info = model.GetTypeInfo(member.Expression);
74+
75+
var stringInfo = model.Compilation.GetTypeByMetadataName(typeof(string).FullName);
76+
77+
if (info.Type.Equals(stringInfo))
78+
{
79+
return GetNewExpression(expression, NodeReplacement.Rename("Equals", "Be"));
80+
}
81+
82+
var ienumerableInfo = model.Compilation.GetTypeByMetadataName(typeof(IEnumerable).FullName);
83+
if (info.Type.AllInterfaces.Contains(ienumerableInfo))
84+
{
85+
return GetNewExpression(expression, NodeReplacement.Rename("Equals", "Equal"));
86+
}
87+
88+
return GetNewExpression(expression, NodeReplacement.Rename("Equals", "Be"));
89+
}
90+
}
91+
}

src/FluentAssertions.Analyzers/Utilities/FluentAssertionsCodeFixProvider.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
2323
foreach (var diagnostic in context.Diagnostics)
2424
{
2525
var expression = (ExpressionSyntax)root.FindNode(diagnostic.Location.SourceSpan);
26-
if (CanRewriteAssertion(expression))
26+
if (await CanRewriteAssertion(expression, context).ConfigureAwait(false))
2727
{
2828
context.RegisterCodeFix(CodeAction.Create(
2929
title: diagnostic.Properties[Constants.DiagnosticProperties.Title],
@@ -33,20 +33,23 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
3333
}
3434
}
3535

36-
protected virtual bool CanRewriteAssertion(ExpressionSyntax expression) => true;
36+
protected virtual Task<bool> CanRewriteAssertion(ExpressionSyntax expression, CodeFixContext context) => Task.FromResult(true);
3737

3838
protected async Task<Document> RewriteAssertion(Document document, ExpressionSyntax expression, ImmutableDictionary<string, string> properties, CancellationToken cancellationToken)
3939
{
4040
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
4141

42-
var newExpression = GetNewExpressionSafely(expression, new FluentAssertionsDiagnosticProperties(properties));
42+
var newExpression = await GetNewExpressionSafelyAsync(expression, document, new FluentAssertionsDiagnosticProperties(properties), cancellationToken);
4343

4444
root = root.ReplaceNode(expression, newExpression);
4545

4646
return document.WithSyntaxRoot(root);
4747
}
4848

49-
protected abstract ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties);
49+
protected virtual Task<ExpressionSyntax> GetNewExpressionAsync(ExpressionSyntax expression, Document document, FluentAssertionsDiagnosticProperties properties, CancellationToken cancellationToken)
50+
=> Task.FromResult(GetNewExpression(expression, properties));
51+
52+
protected virtual ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties) => expression;
5053

5154
protected ExpressionSyntax GetNewExpression(ExpressionSyntax expression, params NodeReplacement[] replacements)
5255
{
@@ -91,16 +94,16 @@ protected ExpressionSyntax RenameIdentifier(ExpressionSyntax expression, string
9194
return expression.ReplaceNode(identifierNode, identifierNode.WithIdentifier(SyntaxFactory.Identifier(newName).WithTriviaFrom(identifierNode.Identifier)));
9295
}
9396

94-
private ExpressionSyntax GetNewExpressionSafely(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)
97+
private Task<ExpressionSyntax> GetNewExpressionSafelyAsync(ExpressionSyntax expression, Document document, FluentAssertionsDiagnosticProperties properties, CancellationToken cancellationToken)
9598
{
9699
try
97100
{
98-
return GetNewExpression(expression, properties);
101+
return GetNewExpressionAsync(expression, document, properties, cancellationToken);
99102
}
100103
catch (Exception e)
101104
{
102105
Console.Error.WriteLine($"Failed to get new expression in {GetType().FullName}.\n{e}");
103-
return expression;
106+
return Task.FromResult(expression);
104107
}
105108
}
106109
}

0 commit comments

Comments
 (0)