Skip to content

Commit 484ccaa

Browse files
authored
Suggest ElementAt only if type implements IList or IReadOnlyList (#63)
1 parent 051ae44 commit 484ccaa

File tree

7 files changed

+498
-309
lines changed

7 files changed

+498
-309
lines changed

src/AwesomeAssertions.Analyzers.TestUtils/AwesomeAssertions.Analyzers.TestUtils.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
5-
5+
<LangVersion>12.0</LangVersion>
66
<IsPackable>false</IsPackable>
77
</PropertyGroup>
88

src/AwesomeAssertions.Analyzers.TestUtils/GenerateCode.cs

Lines changed: 313 additions & 269 deletions
Large diffs are not rendered by default.

src/AwesomeAssertions.Analyzers.Tests/TestAttributes.cs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.VisualStudio.TestTools.UnitTesting;
22
using System;
33
using System.Collections.Generic;
4+
using System.Linq;
45
using System.Reflection;
56

67
namespace AwesomeAssertions.Analyzers.Tests;
@@ -26,17 +27,23 @@ public class AssertionDiagnosticAttribute : Attribute, ITestDataSource
2627
{
2728
public string Assertion { get; }
2829

29-
public AssertionDiagnosticAttribute(string assertion) => Assertion = assertion;
30+
public string[] AdditionalParameters { get; }
31+
32+
public AssertionDiagnosticAttribute(string assertion, params string[] additionalParameters)
33+
{
34+
Assertion = assertion;
35+
AdditionalParameters = additionalParameters ?? Array.Empty<string>();
36+
}
3037

3138
public IEnumerable<object[]> GetData(MethodInfo methodInfo)
3239
{
3340
foreach (var assertion in TestCasesInputUtils.GetTestCases(Assertion))
3441
{
35-
yield return new object[] { assertion };
42+
yield return new object[] { assertion }.Concat(AdditionalParameters).ToArray();
3643
}
3744
}
3845

39-
public string GetDisplayName(MethodInfo methodInfo, object[] data) => $"{methodInfo.Name}(\"{data[0]}\")";
46+
public string GetDisplayName(MethodInfo methodInfo, object[] data) => $"{methodInfo.Name}(\"{string.Join("\", \"", data)}\")";
4047
}
4148

4249
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
@@ -52,18 +59,32 @@ public class AssertionCodeFixAttribute : Attribute, ITestDataSource
5259
{
5360
public string OldAssertion { get; }
5461
public string NewAssertion { get; }
62+
public string[] AdditionalParameters { get; }
5563

56-
public AssertionCodeFixAttribute(string oldAssertion, string newAssertion) => (OldAssertion, NewAssertion) = (oldAssertion, newAssertion);
64+
public AssertionCodeFixAttribute(string oldAssertion, string newAssertion, params string[] additionalParameters)
65+
{
66+
OldAssertion = oldAssertion;
67+
NewAssertion = newAssertion;
68+
AdditionalParameters = additionalParameters ?? Array.Empty<string>();
69+
}
5770

5871
public IEnumerable<object[]> GetData(MethodInfo methodInfo)
5972
{
6073
foreach (var (oldAssertion, newAssertion) in TestCasesInputUtils.GetTestCases(OldAssertion, NewAssertion))
6174
{
62-
yield return new object[] { oldAssertion, newAssertion };
75+
yield return new object[] { oldAssertion, newAssertion }.Concat(AdditionalParameters).ToArray();
6376
}
6477
}
6578

66-
public string GetDisplayName(MethodInfo methodInfo, object[] data) => $"{methodInfo.Name}(\"old: {data[0]}\", new: {data[1]}\")";
79+
public string GetDisplayName(MethodInfo methodInfo, object[] data)
80+
{
81+
if (AdditionalParameters.Length == 0)
82+
{
83+
return $"{methodInfo.Name}(\"old: {data[0]}\", \"new: {data[1]}\")";
84+
}
85+
86+
return $"{methodInfo.Name}(\"old: {data[0]}\", \"new: {data[1]}\", \"{string.Join("\", \"", AdditionalParameters)}\")";
87+
}
6788
}
6889

6990
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]

src/AwesomeAssertions.Analyzers.Tests/Tips/CollectionTests.cs

Lines changed: 123 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -711,10 +711,10 @@ public void CollectionShouldContainSingle_TestAnalyzer_GenericIEnumerableShouldR
711711
Id = AwesomeAssertionsAnalyzer.DiagnosticId,
712712
Message = DiagnosticMetadata.CollectionShouldContainSingle_ShouldHaveCount1.Message,
713713
VisitorName = DiagnosticMetadata.CollectionShouldContainSingle_ShouldHaveCount1.Name,
714-
Locations = new DiagnosticResultLocation[]
715-
{
714+
Locations =
715+
[
716716
new DiagnosticResultLocation("Test0.cs", 11,13)
717-
},
717+
],
718718
Severity = DiagnosticSeverity.Info
719719
});
720720
}
@@ -841,10 +841,12 @@ public void CollectionShouldContainSingle_TestAnalyzer_GenericIEnumerableShouldR
841841
[TestMethod]
842842
[AssertionDiagnostic("actual[0].Children[0].Parent.Should().Be(expectedItem);")]
843843
[AssertionDiagnostic("actual[0].Parent.Should().Be(expectedItem);")]
844+
[Implemented]
844845
public void CollectionShouldHaveElementAt_AccessPropertyOfIndexedValue_TestNoAnalyzer(string assertion) =>
845846
DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(GenerateCode.GenericIListCodeBlockAssertion(assertion));
846847

847848
[TestMethod]
849+
[Implemented]
848850
public void CollectionShouldHaveElementAt_AccessObjectPropertyOfIndexedValue_TestNoAnalyzer()
849851
{
850852
// I cannot see the relevant difference to CollectionShouldHaveElementAt_AccessPropertyOfIndexedValue_TestNoAnalyzer,
@@ -866,6 +868,107 @@ public void TestMethod(List<Value> list)
866868
DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source);
867869
}
868870

871+
[TestMethod]
872+
[Implemented]
873+
public void CollectionShouldHaveElementAt_ClassDoesNotImplementIList_TestNoAnalyzer()
874+
{
875+
string source = """
876+
using AwesomeAssertions;
877+
878+
public class RandomClass
879+
{
880+
public double this[string parameterName] => 42.0;
881+
}
882+
883+
public sealed class TestClass
884+
{
885+
public void TestMethod(RandomClass actual) =>
886+
actual["SomeProperty"].Should().Be(42.0);
887+
}
888+
""";
889+
DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source);
890+
}
891+
892+
[TestMethod]
893+
[AssertionDiagnostic("actual[12].Should().Be(42.0{0});", "double")]
894+
[AssertionDiagnostic("actual[12].Should().Be(new object(){0});", "object")]
895+
[Implemented]
896+
public void CollectionShouldHaveElementAt_ClassImplementsIReadOnlyList_TestAnalyzer(string assertion, string genericType)
897+
{
898+
string source = GenerateCode.GenericAssertionOnClassWhichImplementsIReadOnlyList(genericType, assertion);
899+
900+
var metadata = DiagnosticMetadata.CollectionShouldHaveElementAt_IndexerShouldBe;
901+
DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source, new DiagnosticResult
902+
{
903+
Id = AwesomeAssertionsAnalyzer.DiagnosticId,
904+
Message = metadata.Message,
905+
VisitorName = metadata.Name,
906+
Locations =
907+
[
908+
new DiagnosticResultLocation("Test0.cs", 20, 13)
909+
],
910+
Severity = DiagnosticSeverity.Info
911+
});
912+
}
913+
914+
[TestMethod]
915+
[AssertionCodeFix(
916+
oldAssertion: "actual[12].Should().Be(42.0);",
917+
newAssertion: "actual.Should().HaveElementAt(12, 42.0);",
918+
"double")]
919+
[AssertionCodeFix(
920+
oldAssertion: "actual[12].Should().Be(new object());",
921+
newAssertion: "actual.Should().HaveElementAt(12, new object());",
922+
"object")]
923+
[Implemented]
924+
public void CollectionShouldHaveElementAt_ClassImplementsIReadOnlyList_TestCodeFix(string oldAssertion, string newAssertion, string genericType)
925+
{
926+
string oldSource = GenerateCode.GenericAssertionOnClassWhichImplementsIReadOnlyList(genericType, oldAssertion);
927+
string newSource = GenerateCode.GenericAssertionOnClassWhichImplementsIReadOnlyList(genericType, newAssertion);
928+
929+
VerifyFix(oldSource, newSource);
930+
}
931+
932+
[TestMethod]
933+
[AssertionDiagnostic("actual[12].Should().Be(42.0{0});", "double")]
934+
[AssertionDiagnostic("actual[12].Should().Be(new object(){0});", "object")]
935+
[Implemented]
936+
public void CollectionShouldHaveElementAt_ClassImplementsIList_TestAnalyzer(string assertion, string genericType)
937+
{
938+
string source = GenerateCode.GenericAssertionOnClassWhichImplementsIList(genericType, assertion);
939+
940+
var metadata = DiagnosticMetadata.CollectionShouldHaveElementAt_IndexerShouldBe;
941+
DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source, new DiagnosticResult
942+
{
943+
Id = AwesomeAssertionsAnalyzer.DiagnosticId,
944+
Message = metadata.Message,
945+
VisitorName = metadata.Name,
946+
Locations =
947+
[
948+
new DiagnosticResultLocation("Test0.cs", 33, 13)
949+
],
950+
Severity = DiagnosticSeverity.Info
951+
});
952+
}
953+
954+
[TestMethod]
955+
[AssertionCodeFix(
956+
oldAssertion: "actual[12].Should().Be(42.0);",
957+
newAssertion: "actual.Should().HaveElementAt(12, 42.0);",
958+
"double")]
959+
[AssertionCodeFix(
960+
oldAssertion: "actual[12].Should().Be(new object());",
961+
newAssertion: "actual.Should().HaveElementAt(12, new object());",
962+
"object")]
963+
[Implemented]
964+
public void CollectionShouldHaveElementAt_ClassImplementsIList_TestCodeFix(string oldAssertion, string newAssertion, string genericType)
965+
{
966+
string oldSource = GenerateCode.GenericAssertionOnClassWhichImplementsIList(genericType, oldAssertion);
967+
string newSource = GenerateCode.GenericAssertionOnClassWhichImplementsIList(genericType, newAssertion);
968+
969+
VerifyFix(oldSource, newSource);
970+
}
971+
869972
[TestMethod]
870973
[AssertionDiagnostic("var first = actual[0]; first[6].Should().Be(expectedItem{0});")]
871974
[Implemented]
@@ -1132,7 +1235,7 @@ public void TestMethod(List<Value> list)
11321235
[Ignore("What Should Happen?")]
11331236
public void CollectionShouldHaveElementAt0Null_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFixCodeBlock(oldAssertion, newAssertion);
11341237

1135-
private void VerifyCSharpDiagnosticCodeBlock(string sourceAssertion, DiagnosticMetadata metadata)
1238+
private static void VerifyCSharpDiagnosticCodeBlock(string sourceAssertion, DiagnosticMetadata metadata)
11361239
{
11371240
var source = GenerateCode.GenericIListCodeBlockAssertion(sourceAssertion);
11381241

@@ -1141,15 +1244,15 @@ private void VerifyCSharpDiagnosticCodeBlock(string sourceAssertion, DiagnosticM
11411244
Id = AwesomeAssertionsAnalyzer.DiagnosticId,
11421245
Message = metadata.Message,
11431246
VisitorName = metadata.Name,
1144-
Locations = new DiagnosticResultLocation[]
1145-
{
1247+
Locations =
1248+
[
11461249
new DiagnosticResultLocation("Test0.cs", 11,13)
1147-
},
1250+
],
11481251
Severity = DiagnosticSeverity.Info
11491252
});
11501253
}
11511254

1152-
private void VerifyCSharpDiagnosticExpressionBody(string sourceAssertion, DiagnosticMetadata metadata)
1255+
private static void VerifyCSharpDiagnosticExpressionBody(string sourceAssertion, DiagnosticMetadata metadata)
11531256
{
11541257
var source = GenerateCode.GenericIListExpressionBodyAssertion(sourceAssertion);
11551258

@@ -1158,15 +1261,15 @@ private void VerifyCSharpDiagnosticExpressionBody(string sourceAssertion, Diagno
11581261
Id = AwesomeAssertionsAnalyzer.DiagnosticId,
11591262
VisitorName = metadata.Name,
11601263
Message = metadata.Message,
1161-
Locations = new DiagnosticResultLocation[]
1162-
{
1163-
new DiagnosticResultLocation("Test0.cs", 10,16)
1164-
},
1264+
Locations =
1265+
[
1266+
new DiagnosticResultLocation("Test0.cs", 10, 28)
1267+
],
11651268
Severity = DiagnosticSeverity.Info
11661269
});
11671270
}
11681271

1169-
private void VerifyArrayCSharpDiagnosticCodeBlock(string sourceAssertion, DiagnosticMetadata metadata)
1272+
private static void VerifyArrayCSharpDiagnosticCodeBlock(string sourceAssertion, DiagnosticMetadata metadata)
11701273
{
11711274
var source = GenerateCode.GenericArrayCodeBlockAssertion(sourceAssertion);
11721275

@@ -1175,39 +1278,39 @@ private void VerifyArrayCSharpDiagnosticCodeBlock(string sourceAssertion, Diagno
11751278
Id = AwesomeAssertionsAnalyzer.DiagnosticId,
11761279
Message = metadata.Message,
11771280
VisitorName = metadata.Name,
1178-
Locations = new DiagnosticResultLocation[]
1179-
{
1281+
Locations =
1282+
[
11801283
new DiagnosticResultLocation("Test0.cs", 11,13)
1181-
},
1284+
],
11821285
Severity = DiagnosticSeverity.Info
11831286
});
11841287
}
11851288

1186-
private void VerifyArrayCSharpFixCodeBlock(string oldSourceAssertion, string newSourceAssertion)
1289+
private static void VerifyArrayCSharpFixCodeBlock(string oldSourceAssertion, string newSourceAssertion)
11871290
{
11881291
var oldSource = GenerateCode.GenericArrayCodeBlockAssertion(oldSourceAssertion);
11891292
var newSource = GenerateCode.GenericArrayCodeBlockAssertion(newSourceAssertion);
11901293

11911294
VerifyFix(oldSource, newSource);
11921295
}
11931296

1194-
private void VerifyCSharpFixCodeBlock(string oldSourceAssertion, string newSourceAssertion)
1297+
private static void VerifyCSharpFixCodeBlock(string oldSourceAssertion, string newSourceAssertion)
11951298
{
11961299
var oldSource = GenerateCode.GenericIListCodeBlockAssertion(oldSourceAssertion);
11971300
var newSource = GenerateCode.GenericIListCodeBlockAssertion(newSourceAssertion);
11981301

11991302
VerifyFix(oldSource, newSource);
12001303
}
12011304

1202-
private void VerifyCSharpFixExpressionBody(string oldSourceAssertion, string newSourceAssertion)
1305+
private static void VerifyCSharpFixExpressionBody(string oldSourceAssertion, string newSourceAssertion)
12031306
{
12041307
var oldSource = GenerateCode.GenericIListExpressionBodyAssertion(oldSourceAssertion);
12051308
var newSource = GenerateCode.GenericIListExpressionBodyAssertion(newSourceAssertion);
12061309

12071310
VerifyFix(oldSource, newSource);
12081311
}
12091312

1210-
private void VerifyFix(string oldSource, string newSource)
1313+
private static void VerifyFix(string oldSource, string newSource)
12111314
{
12121315
DiagnosticVerifier.VerifyFix(new CodeFixVerifierArguments()
12131316
.WithCodeFixProvider<AwesomeAssertionsCodeFixProvider>()

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ public void TestMethod(MultiKeyDict<int, int, string> actual)
185185
public void DictionaryShouldContainPair_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion);
186186

187187
[TestMethod]
188+
[Implemented]
188189
public void DictionaryWithIntValue_ShouldBe_NoDiagnostic()
189190
{
190191
string assertion = "actual[\"one\"].Should().Be(1);";

src/AwesomeAssertions.Analyzers/Tips/AwesomeAssertionsAnalyzer.cs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, AwesomeA
179179
case nameof(Enumerable.OrderByDescending) when IsEnumerableMethodWithPredicate(invocationBeforeShould, metadata) && invocationBeforeShould.Arguments[0].IsSameArgumentReference(assertion.Arguments[0]):
180180
context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.CollectionShouldBeInDescendingOrder_OrderByDescendingShouldEqual));
181181
return;
182-
case nameof(Enumerable.Select) when IsEnumerableMethodWithPredicate(invocationBeforeShould, metadata)
182+
case nameof(Enumerable.Select) when IsEnumerableMethodWithPredicate(invocationBeforeShould, metadata)
183183
&& assertion.Arguments[0].Value is IInvocationOperation { TargetMethod.Name: nameof(Enumerable.Select), Arguments.Length: 2 } expectedInvocation && expectedInvocation.Arguments[1].IsLambda():
184184
context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.CollectionShouldEqualOtherCollectionByComparer_SelectShouldEqualOtherCollectionSelect));
185185
return;
@@ -361,8 +361,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, AwesomeA
361361
}
362362
}
363363
if (subject is IPropertyReferenceOperation propertyReference && propertyReference.Property.Name is WellKnownMemberNames.Indexer
364-
&& !(subject.Type.AllInterfaces.Contains(metadata.IDictionaryOfT2) || subject.Type.AllInterfaces.Contains(metadata.IReadonlyDictionaryOfT2))
365-
&& !(propertyReference.Property.ContainingType.ImplementsOrIsInterface(metadata.IDictionaryOfT2) || propertyReference.Property.ContainingType.ImplementsOrIsInterface(metadata.IReadonlyDictionaryOfT2)))
364+
&& (propertyReference.Instance.Type.ImplementsOrIsInterface(metadata.IListOfT) || propertyReference.Instance.Type.ImplementsOrIsInterface(metadata.IReadonlyListOfT)))
366365
{
367366
context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.CollectionShouldHaveElementAt_IndexerShouldBe));
368367
}
@@ -389,23 +388,17 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, AwesomeA
389388
}
390389
}
391390

392-
if (subject is IPropertyReferenceOperation propertyReferenceOperation)
393-
{
394-
return;
395-
}
396-
else if (subject.TryGetSingleChild<IPropertyReferenceOperation>(out var previousPropertyReference) && !previousPropertyReference.Property.IsIndexer)
391+
if (subject.TryGetSingleChildOrSelf<IPropertyReferenceOperation>(out var propertyReference) && !propertyReference.Property.IsIndexer)
397392
{
398393
return;
399394
}
400395
else if (subject.TryGetFirstDescendent<IArrayElementReferenceOperation>(out _))
401396
{
402397
context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.CollectionShouldHaveElementAt_IndexerShouldBe));
403398
}
404-
else if (subject.TryGetFirstDescendent<IPropertyReferenceOperation>(out var propertyReference) && propertyReference.Property.IsIndexer)
399+
else if (subject.TryGetFirstDescendentOrSelf(out propertyReference) && propertyReference.Property.IsIndexer &&
400+
(propertyReference.Instance.Type.ImplementsOrIsInterface(metadata.IListOfT) || propertyReference.Instance.Type.ImplementsOrIsInterface(metadata.IReadonlyListOfT)))
405401
{
406-
if (!propertyReference.Instance.Type.ImplementsOrIsInterface(metadata.IListOfT) && !propertyReference.Instance.Type.ImplementsOrIsInterface(metadata.IReadonlyListOfT)) return;
407-
if (propertyReference.Instance.Type.ImplementsOrIsInterface(metadata.IDictionaryOfT2) || propertyReference.Instance.Type.ImplementsOrIsInterface(metadata.IReadonlyDictionaryOfT2)) return;
408-
409402
context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.CollectionShouldHaveElementAt_IndexerShouldBe));
410403
}
411404
}
@@ -494,7 +487,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, AwesomeA
494487
context.ReportDiagnostic(CreateDiagnostic(assertion, DiagnosticMetadata.CollectionShouldHaveCountLessThanOrEqualTo_CountShouldBeLessThanOrEqualTo));
495488
return;
496489
case nameof(Math.Abs) when invocationBeforeShould.IsContainedInType(metadata.Math) && (
497-
assertion.Arguments[0].IsReferenceOfType(SpecialType.System_Double)
490+
assertion.Arguments[0].IsReferenceOfType(SpecialType.System_Double)
498491
|| assertion.Arguments[0].IsReferenceOfType(SpecialType.System_Single)
499492
|| assertion.Arguments[0].IsReferenceOfType(SpecialType.System_Decimal)
500493
):

0 commit comments

Comments
 (0)