Skip to content

Commit 795064d

Browse files
authored
feat: Migrate equals to IOperation (#272)
1 parent a5e6464 commit 795064d

File tree

7 files changed

+73
-109
lines changed

7 files changed

+73
-109
lines changed

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

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using FluentAssertions.Analyzers.TestUtils;
2-
using FluentAssertions.Analyzers.Tips;
32
using Microsoft.CodeAnalysis;
43
using Microsoft.VisualStudio.TestTools.UnitTesting;
54

@@ -11,7 +10,7 @@ public class ShouldEqualsTests
1110
[TestMethod]
1211
[Implemented]
1312
public void ShouldEquals_TestAnalyzer()
14-
=> VerifyCSharpDiagnosticExpressionBody("actual.Should().Equals(expected);");
13+
=> VerifyCSharpDiagnosticExpressionBody("actual.Should().Equals(expected);", DiagnosticMetadata.ShouldBe_ShouldEquals);
1514

1615
[TestMethod]
1716
[Implemented]
@@ -20,13 +19,13 @@ public void ShouldEquals_ShouldBe_ObjectType_TestCodeFix()
2019
var oldSource = GenerateCode.ObjectStatement("actual.Should().Equals(expected);");
2120
var newSource = GenerateCode.ObjectStatement("actual.Should().Be(expected);");
2221

23-
DiagnosticVerifier.VerifyCSharpFix<ShouldEqualsCodeFix, ShouldEqualsAnalyzer>(oldSource, newSource);
22+
DiagnosticVerifier.VerifyCSharpFix<FluentAssertionsCodeFix, FluentAssertionsOperationAnalyzer>(oldSource, newSource);
2423
}
2524

2625
[TestMethod]
2726
[Implemented]
2827
public void ShouldEquals_NestedInsideIfBlock_TestAnalyzer()
29-
=> VerifyCSharpDiagnosticExpressionBody("if(true) { actual.Should().Equals(expected); }", 10, 24);
28+
=> VerifyCSharpDiagnosticExpressionBody("if(true) { actual.Should().Equals(expected); }", 10, 24, DiagnosticMetadata.ShouldBe_ShouldEquals);
3029

3130
[TestMethod]
3231
[Implemented]
@@ -35,13 +34,13 @@ public void ShouldEquals_NestedInsideIfBlock_ShouldBe_ObjectType_TestCodeFix()
3534
var oldSource = GenerateCode.ObjectStatement("if(true) { actual.Should().Equals(expected); }");
3635
var newSource = GenerateCode.ObjectStatement("if(true) { actual.Should().Be(expected); }");
3736

38-
DiagnosticVerifier.VerifyCSharpFix<ShouldEqualsCodeFix, ShouldEqualsAnalyzer>(oldSource, newSource);
37+
DiagnosticVerifier.VerifyCSharpFix<FluentAssertionsCodeFix, FluentAssertionsOperationAnalyzer>(oldSource, newSource);
3938
}
4039

4140
[TestMethod]
4241
[Implemented]
4342
public void ShouldEquals_NestedInsideWhileBlock_TestAnalyzer()
44-
=> VerifyCSharpDiagnosticExpressionBody("while(true) { actual.Should().Equals(expected); }", 10, 27);
43+
=> VerifyCSharpDiagnosticExpressionBody("while(true) { actual.Should().Equals(expected); }", 10, 27, DiagnosticMetadata.ShouldBe_ShouldEquals);
4544

4645
[TestMethod]
4746
[Implemented]
@@ -50,14 +49,14 @@ public void ShouldEquals_NestedInsideWhileBlock_ShouldBe_ObjectType_TestCodeFix(
5049
var oldSource = GenerateCode.ObjectStatement("while(true) { actual.Should().Equals(expected); }");
5150
var newSource = GenerateCode.ObjectStatement("while(true) { actual.Should().Be(expected); }");
5251

53-
DiagnosticVerifier.VerifyCSharpFix<ShouldEqualsCodeFix, ShouldEqualsAnalyzer>(oldSource, newSource);
52+
DiagnosticVerifier.VerifyCSharpFix<FluentAssertionsCodeFix, FluentAssertionsOperationAnalyzer>(oldSource, newSource);
5453
}
5554

5655
[TestMethod]
5756
[Implemented]
5857
public void ShouldEquals_ActualIsMethodInvoaction_TestAnalyzer()
5958
=> VerifyCSharpDiagnosticExpressionBody("object ResultSupplier() { return null; } \n"
60-
+ "ResultSupplier().Should().Equals(expected);", 11, 0);
59+
+ "ResultSupplier().Should().Equals(expected);", 11, 0, DiagnosticMetadata.ShouldBe_ShouldEquals);
6160

6261
[TestMethod]
6362
[Implemented]
@@ -67,7 +66,7 @@ public void ShouldEquals_ActualIsMethodInvoaction_ShouldBe_ObjectType_TestCodeFi
6766
var oldSource = GenerateCode.ObjectStatement(methodInvocation + "ResultSupplier().Should().Equals(expected);");
6867
var newSource = GenerateCode.ObjectStatement(methodInvocation + "ResultSupplier().Should().Be(expected);");
6968

70-
DiagnosticVerifier.VerifyCSharpFix<ShouldEqualsCodeFix, ShouldEqualsAnalyzer>(oldSource, newSource);
69+
DiagnosticVerifier.VerifyCSharpFix<FluentAssertionsCodeFix, FluentAssertionsOperationAnalyzer>(oldSource, newSource);
7170
}
7271

7372
[TestMethod]
@@ -77,7 +76,7 @@ public void ShouldEquals_ShouldBe_NumberType_TestCodeFix()
7776
var oldSource = GenerateCode.DoubleAssertion("actual.Should().Equals(expected);");
7877
var newSource = GenerateCode.DoubleAssertion("actual.Should().Be(expected);");
7978

80-
DiagnosticVerifier.VerifyCSharpFix<ShouldEqualsCodeFix, ShouldEqualsAnalyzer>(oldSource, newSource);
79+
DiagnosticVerifier.VerifyCSharpFix<FluentAssertionsCodeFix, FluentAssertionsOperationAnalyzer>(oldSource, newSource);
8180
}
8281

8382
[TestMethod]
@@ -87,7 +86,7 @@ public void ShouldEquals_ShouldBe_StringType_TestCodeFix()
8786
var oldSource = GenerateCode.StringAssertion("actual.Should().Equals(expected);");
8887
var newSource = GenerateCode.StringAssertion("actual.Should().Be(expected);");
8988

90-
DiagnosticVerifier.VerifyCSharpFix<ShouldEqualsCodeFix, ShouldEqualsAnalyzer>(oldSource, newSource);
89+
DiagnosticVerifier.VerifyCSharpFix<FluentAssertionsCodeFix, FluentAssertionsOperationAnalyzer>(oldSource, newSource);
9190
}
9291

9392
[TestMethod]
@@ -97,17 +96,18 @@ public void ShouldEquals_ShouldEqual_EnumerableType_TestCodeFix()
9796
var oldSource = GenerateCode.GenericIListCodeBlockAssertion("actual.Should().Equals(expected);");
9897
var newSource = GenerateCode.GenericIListCodeBlockAssertion("actual.Should().Equal(expected);");
9998

100-
DiagnosticVerifier.VerifyCSharpFix<ShouldEqualsCodeFix, ShouldEqualsAnalyzer>(oldSource, newSource);
99+
DiagnosticVerifier.VerifyCSharpFix<FluentAssertionsCodeFix, FluentAssertionsOperationAnalyzer>(oldSource, newSource);
101100
}
102101

103-
private void VerifyCSharpDiagnosticExpressionBody(string sourceAssertion) => VerifyCSharpDiagnosticExpressionBody(sourceAssertion, 10, 13);
104-
private void VerifyCSharpDiagnosticExpressionBody(string sourceAssertion, int line, int column)
102+
private void VerifyCSharpDiagnosticExpressionBody(string sourceAssertion, DiagnosticMetadata metadata) => VerifyCSharpDiagnosticExpressionBody(sourceAssertion, 10, 13, metadata);
103+
private void VerifyCSharpDiagnosticExpressionBody(string sourceAssertion, int line, int column, DiagnosticMetadata metadata)
105104
{
106105
var source = GenerateCode.ObjectStatement(sourceAssertion);
107106
DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source, new DiagnosticResult
108107
{
109-
Id = ShouldEqualsAnalyzer.DiagnosticId,
110-
Message = ShouldEqualsAnalyzer.Message,
108+
Id = FluentAssertionsOperationAnalyzer.DiagnosticId,
109+
Message = metadata.Message,
110+
VisitorName = metadata.Name,
111111
Locations = new DiagnosticResultLocation[]
112112
{
113113
new DiagnosticResultLocation("Test0.cs", line, column)

src/FluentAssertions.Analyzers/Tips/DiagnosticMetadata.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ private DiagnosticMetadata(string message, string helpLink, [CallerMemberName] s
104104
public static DiagnosticMetadata ExceptionShouldThrowExactlyWithInnerException_ShouldThrowExactlyAndInnerExceptionShouldBeAssignableTo = new("Use .Should().ThrowExactly<TException>().WithInnerException<TInnerException>()", string.Empty);
105105
public static DiagnosticMetadata ExceptionShouldThrowExactlyWithInnerException_ShouldThrowExactlyWhichInnerExceptionShouldBeAssignableTo = new("Use .Should().ThrowExactly<TException>().WithInnerException<TInnerException>()", string.Empty);
106106

107+
public static DiagnosticMetadata StringShouldBe_StringShouldEquals = new("Use .Should().Be()", string.Empty);
108+
public static DiagnosticMetadata CollectionShouldEqual_CollectionShouldEquals = new("Use .Should().Equal()", string.Empty);
109+
public static DiagnosticMetadata ShouldEquals = new("Use .Should().Be() or .Should().BeEquivalentTo or .Should().Equal() or other equivalency assertion", string.Empty);
110+
public static DiagnosticMetadata ShouldBe_ShouldEquals = new("Use .Should().Be()", string.Empty);
107111

108112
private static string GetHelpLink(string id) => $"https://fluentassertions.com/tips/#{id}";
109113
}

src/FluentAssertions.Analyzers/Tips/FluentAssertionsCodeFix.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections.Immutable;
22
using System.Composition;
33
using System.Linq;
4+
using System.Threading.Tasks;
45
using Microsoft.CodeAnalysis;
56
using Microsoft.CodeAnalysis.CodeFixes;
67
using Microsoft.CodeAnalysis.CSharp;
@@ -13,6 +14,20 @@ public partial class FluentAssertionsCodeFix : FluentAssertionsCodeFixProvider
1314
{
1415
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(FluentAssertionsOperationAnalyzer.DiagnosticId);
1516

17+
protected override Task<bool> CanRewriteAssertion(ExpressionSyntax expression, CodeFixContext context, Diagnostic diagnostic)
18+
{
19+
if (diagnostic.Properties.TryGetValue(Constants.DiagnosticProperties.VisitorName, out var visitorName))
20+
{
21+
return visitorName switch
22+
{
23+
nameof(DiagnosticMetadata.ShouldEquals) => Task.FromResult(false),
24+
_ => Task.FromResult(true),
25+
};
26+
}
27+
28+
return base.CanRewriteAssertion(expression, context, diagnostic);
29+
}
30+
1631
protected override ExpressionSyntax GetNewExpression(ExpressionSyntax expression, FluentAssertionsDiagnosticProperties properties)
1732
{
1833
// oldAssertion: subject.<remove>(arg1).Should().<rename>(arg2, {reasonArgs});
@@ -337,7 +352,11 @@ ExpressionSyntax GetCombinedAssertionsWithArgumentsReversedOrder(string remove,
337352
case nameof(DiagnosticMetadata.ExceptionShouldThrowWithMessage_ShouldThrowAndMessageShouldEndWith):
338353
case nameof(DiagnosticMetadata.ExceptionShouldThrowExactlyWithMessage_ShouldThrowExactlyAndMessageShouldEndWith):
339354
return ReplaceEndWithMessage(expression, "And");
340-
355+
case nameof(DiagnosticMetadata.CollectionShouldEqual_CollectionShouldEquals):
356+
return GetNewExpression(expression, NodeReplacement.Rename("Equals", "Equal"));
357+
case nameof(DiagnosticMetadata.StringShouldBe_StringShouldEquals):
358+
case nameof(DiagnosticMetadata.ShouldBe_ShouldEquals):
359+
return GetNewExpression(expression, NodeReplacement.Rename("Equals", "Be"));
341360
default: throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}");
342361
};
343362
}

src/FluentAssertions.Analyzers/Tips/FluentAssertionsOperationAnalyzer.Utils.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4+
using System.IO;
45
using System.Linq;
6+
using System.Threading.Tasks;
57
using Microsoft.CodeAnalysis;
68
using Microsoft.CodeAnalysis.Operations;
79

@@ -52,7 +54,10 @@ public FluentAssertionsMetadata(Compilation compilation)
5254
DictionaryOfT2 = compilation.GetTypeByMetadataName(typeof(Dictionary<,>).FullName);
5355
IReadonlyDictionaryOfT2 = compilation.GetTypeByMetadataName(typeof(IReadOnlyDictionary<,>).FullName);
5456
Enumerable = compilation.GetTypeByMetadataName(typeof(Enumerable).FullName);
57+
IEnumerable = compilation.GetTypeByMetadataName(typeof(IEnumerable).FullName);
5558
Math = compilation.GetTypeByMetadataName(typeof(Math).FullName);
59+
TaskCompletionSourceOfT1 = compilation.GetTypeByMetadataName(typeof(TaskCompletionSource<>).FullName);
60+
Stream = compilation.GetTypeByMetadataName(typeof(Stream).FullName);
5661
}
5762
public INamedTypeSymbol AssertionExtensions { get; }
5863
public INamedTypeSymbol ReferenceTypeAssertionsOfT2 { get; }
@@ -68,6 +73,9 @@ public FluentAssertionsMetadata(Compilation compilation)
6873
public INamedTypeSymbol BooleanAssertionsOfT1 { get; }
6974
public INamedTypeSymbol NumericAssertionsOfT2 { get; }
7075
public INamedTypeSymbol Enumerable { get; }
76+
public INamedTypeSymbol IEnumerable { get; }
7177
public INamedTypeSymbol Math { get; }
78+
public INamedTypeSymbol TaskCompletionSourceOfT1 { get; }
79+
public INamedTypeSymbol Stream { get; }
7280
}
7381
}

src/FluentAssertions.Analyzers/Tips/FluentAssertionsOperationAnalyzer.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,29 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, FluentAs
5454

5555
switch (assertion.TargetMethod.Name)
5656
{
57+
case nameof(object.Equals):
58+
59+
if (subject.Type.SpecialType is SpecialType.System_String)
60+
{
61+
context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.StringShouldBe_StringShouldEquals));
62+
return;
63+
}
64+
65+
if (subject.Type.EqualsSymbol(metadata.TaskCompletionSourceOfT1)
66+
|| subject.Type.AllInterfaces.Contains(metadata.Stream))
67+
{
68+
context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.ShouldEquals));
69+
return;
70+
}
71+
72+
if (subject.Type.ImplementsOrIsInterface(metadata.IEnumerable))
73+
{
74+
context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.CollectionShouldEqual_CollectionShouldEquals));
75+
return;
76+
}
77+
78+
context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.ShouldBe_ShouldEquals));
79+
return;
5780
case "NotBeEmpty" when assertion.IsContainedInType(metadata.GenericCollectionAssertionsOfT3):
5881
{
5982
if (assertion.TryGetChainedInvocationAfterAndConstraint("NotBeNull", out var chainedInvocation))

src/FluentAssertions.Analyzers/Tips/ShouldEquals.cs

Lines changed: 0 additions & 90 deletions
This file was deleted.

src/FluentAssertions.Analyzers/Utilities/FluentAssertionsCodeFixProvider.cs

Lines changed: 2 additions & 2 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 (await CanRewriteAssertion(expression, context).ConfigureAwait(false))
26+
if (await CanRewriteAssertion(expression, context, diagnostic).ConfigureAwait(false))
2727
{
2828
context.RegisterCodeFix(CodeAction.Create(
2929
title: diagnostic.Properties[Constants.DiagnosticProperties.Title],
@@ -33,7 +33,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
3333
}
3434
}
3535

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

3838
protected async Task<Document> RewriteAssertion(Document document, ExpressionSyntax expression, ImmutableDictionary<string, string> properties, CancellationToken cancellationToken)
3939
{

0 commit comments

Comments
 (0)