Skip to content

Commit baac22c

Browse files
Fix analyzer false positives for collection asserts by @Youssef1313 in #6300 (backport to rel/3.10) (#6301)
Co-authored-by: Youssef1313 <youssefvictor00@gmail.com>
1 parent e5893ee commit baac22c

File tree

2 files changed

+92
-19
lines changed

2 files changed

+92
-19
lines changed

src/Analyzers/MSTest.Analyzers/UseProperAssertMethodsAnalyzer.cs

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,13 @@ public override void Initialize(AnalysisContext context)
219219
return;
220220
}
221221

222-
context.RegisterOperationAction(context => AnalyzeInvocationOperation(context, assertTypeSymbol), OperationKind.Invocation);
222+
INamedTypeSymbol objectTypeSymbol = context.Compilation.GetSpecialType(SpecialType.System_Object);
223+
224+
context.RegisterOperationAction(context => AnalyzeInvocationOperation(context, assertTypeSymbol, objectTypeSymbol), OperationKind.Invocation);
223225
});
224226
}
225227

226-
private static void AnalyzeInvocationOperation(OperationAnalysisContext context, INamedTypeSymbol assertTypeSymbol)
228+
private static void AnalyzeInvocationOperation(OperationAnalysisContext context, INamedTypeSymbol assertTypeSymbol, INamedTypeSymbol objectTypeSymbol)
227229
{
228230
var operation = (IInvocationOperation)context.Operation;
229231
IMethodSymbol targetMethod = operation.TargetMethod;
@@ -240,19 +242,19 @@ private static void AnalyzeInvocationOperation(OperationAnalysisContext context,
240242
switch (targetMethod.Name)
241243
{
242244
case "IsTrue":
243-
AnalyzeIsTrueOrIsFalseInvocation(context, firstArgument, isTrueInvocation: true);
245+
AnalyzeIsTrueOrIsFalseInvocation(context, firstArgument, isTrueInvocation: true, objectTypeSymbol);
244246
break;
245247

246248
case "IsFalse":
247-
AnalyzeIsTrueOrIsFalseInvocation(context, firstArgument, isTrueInvocation: false);
249+
AnalyzeIsTrueOrIsFalseInvocation(context, firstArgument, isTrueInvocation: false, objectTypeSymbol);
248250
break;
249251

250252
case "AreEqual":
251-
AnalyzeAreEqualOrAreNotEqualInvocation(context, firstArgument, isAreEqualInvocation: true);
253+
AnalyzeAreEqualOrAreNotEqualInvocation(context, firstArgument, isAreEqualInvocation: true, objectTypeSymbol);
252254
break;
253255

254256
case "AreNotEqual":
255-
AnalyzeAreEqualOrAreNotEqualInvocation(context, firstArgument, isAreEqualInvocation: false);
257+
AnalyzeAreEqualOrAreNotEqualInvocation(context, firstArgument, isAreEqualInvocation: false, objectTypeSymbol);
256258
break;
257259
}
258260
}
@@ -429,6 +431,7 @@ private static StringMethodCheckStatus RecognizeStringMethodCheck(
429431

430432
private static CollectionCheckStatus RecognizeCollectionMethodCheck(
431433
IOperation operation,
434+
INamedTypeSymbol objectTypeSymbol,
432435
out SyntaxNode? collectionExpression,
433436
out SyntaxNode? itemExpression)
434437
{
@@ -439,10 +442,7 @@ private static CollectionCheckStatus RecognizeCollectionMethodCheck(
439442
// Check for Collection.Contains(item)
440443
if (methodName == "Contains" && invocation.Arguments.Length == 1)
441444
{
442-
// Ensure it's a collection type (implements IEnumerable)
443-
ITypeSymbol? receiverType = invocation.Instance?.Type;
444-
if (receiverType is not null &&
445-
IsCollectionType(receiverType))
445+
if (IsBCLCollectionType(invocation.TargetMethod.ContainingType, objectTypeSymbol))
446446
{
447447
collectionExpression = invocation.Instance?.Syntax;
448448
itemExpression = invocation.Arguments[0].Value.Syntax;
@@ -456,11 +456,14 @@ private static CollectionCheckStatus RecognizeCollectionMethodCheck(
456456
return CollectionCheckStatus.Unknown;
457457
}
458458

459-
private static bool IsCollectionType(ITypeSymbol type)
460-
// Check if the type implements IEnumerable (but is not string)
459+
private static bool IsBCLCollectionType(ITypeSymbol type, INamedTypeSymbol objectTypeSymbol)
460+
// Check if the type implements IEnumerable<T> (but is not string)
461+
// Note: Assert.Contains/IsEmpty/HasCount for collections accept IEnumerable<T>, but not IEnumerable.
461462
=> type.SpecialType != SpecialType.System_String && type.AllInterfaces.Any(i =>
462-
i.SpecialType == SpecialType.System_Collections_IEnumerable ||
463-
(i.OriginalDefinition?.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T));
463+
i.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T) &&
464+
// object is coming from BCL and it's expected to always have a public key.
465+
type.ContainingAssembly.Identity.HasPublicKey == objectTypeSymbol.ContainingAssembly.Identity.HasPublicKey &&
466+
type.ContainingAssembly.Identity.PublicKey.SequenceEqual(objectTypeSymbol.ContainingAssembly.Identity.PublicKey);
464467

465468
private static ComparisonCheckStatus RecognizeComparisonCheck(
466469
IOperation operation,
@@ -488,7 +491,7 @@ private static ComparisonCheckStatus RecognizeComparisonCheck(
488491
return ComparisonCheckStatus.Unknown;
489492
}
490493

491-
private static void AnalyzeIsTrueOrIsFalseInvocation(OperationAnalysisContext context, IOperation conditionArgument, bool isTrueInvocation)
494+
private static void AnalyzeIsTrueOrIsFalseInvocation(OperationAnalysisContext context, IOperation conditionArgument, bool isTrueInvocation, INamedTypeSymbol objectTypeSymbol)
492495
{
493496
RoslynDebug.Assert(context.Operation is IInvocationOperation, "Expected IInvocationOperation.");
494497

@@ -575,7 +578,7 @@ private static void AnalyzeIsTrueOrIsFalseInvocation(OperationAnalysisContext co
575578
}
576579

577580
// Check for collection method patterns: myCollection.Contains(...)
578-
CollectionCheckStatus collectionMethodStatus = RecognizeCollectionMethodCheck(conditionArgument, out SyntaxNode? collectionExpr, out SyntaxNode? itemExpr);
581+
CollectionCheckStatus collectionMethodStatus = RecognizeCollectionMethodCheck(conditionArgument, objectTypeSymbol, out SyntaxNode? collectionExpr, out SyntaxNode? itemExpr);
579582
if (collectionMethodStatus != CollectionCheckStatus.Unknown)
580583
{
581584
if (collectionMethodStatus == CollectionCheckStatus.Contains)
@@ -705,7 +708,7 @@ private static void AnalyzeIsTrueOrIsFalseInvocation(OperationAnalysisContext co
705708
}
706709
}
707710

708-
private static void AnalyzeAreEqualOrAreNotEqualInvocation(OperationAnalysisContext context, IOperation expectedArgument, bool isAreEqualInvocation)
711+
private static void AnalyzeAreEqualOrAreNotEqualInvocation(OperationAnalysisContext context, IOperation expectedArgument, bool isAreEqualInvocation, INamedTypeSymbol objectTypeSymbol)
709712
{
710713
// Check for collection count patterns: collection.Count/Length == 0 or collection.Count/Length == X
711714
if (isAreEqualInvocation)
@@ -716,6 +719,7 @@ private static void AnalyzeAreEqualOrAreNotEqualInvocation(OperationAnalysisCont
716719
CountCheckStatus countStatus = RecognizeCountCheck(
717720
expectedArgument,
718721
actualArgumentValue,
722+
objectTypeSymbol,
719723
out SyntaxNode? collectionExpr,
720724
out _,
721725
out _);
@@ -822,6 +826,7 @@ actualArgumentValue.Type is { } actualType &&
822826
private static CountCheckStatus RecognizeCountCheck(
823827
IOperation expectedArgument,
824828
IOperation actualArgument,
829+
INamedTypeSymbol objectTypeSymbol,
825830
out SyntaxNode? collectionExpression,
826831
out SyntaxNode? countExpression,
827832
out int countValue)
@@ -833,7 +838,7 @@ expectedArgument.ConstantValue.Value is int expectedValue &&
833838
actualArgument is IPropertyReferenceOperation propertyRef &&
834839
propertyRef.Property.Name is "Count" or "Length" &&
835840
propertyRef.Instance?.Type is not null &&
836-
IsCollectionType(propertyRef.Instance.Type))
841+
IsBCLCollectionType(propertyRef.Property.ContainingType, objectTypeSymbol))
837842
{
838843
collectionExpression = propertyRef.Instance.Syntax;
839844
countExpression = propertyRef.Syntax;
@@ -848,7 +853,7 @@ actualArgument.ConstantValue.Value is int actualValue &&
848853
expectedArgument is IPropertyReferenceOperation propertyRef2 &&
849854
propertyRef2.Property.Name is "Count" or "Length" &&
850855
propertyRef2.Instance?.Type is not null &&
851-
IsCollectionType(propertyRef2.Instance.Type))
856+
IsBCLCollectionType(propertyRef2.Property.ContainingType, objectTypeSymbol))
852857
{
853858
collectionExpression = propertyRef2.Instance.Syntax;
854859
countExpression = propertyRef2.Syntax;

test/UnitTests/MSTest.Analyzers.UnitTests/UseProperAssertMethodsAnalyzerTests.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2091,5 +2091,73 @@ await VerifyCS.VerifyCodeFixAsync(
20912091
fixedCode);
20922092
}
20932093

2094+
[TestMethod]
2095+
public async Task WhenAssertAreEqualWithCollectionCountUsingCustomCollection()
2096+
{
2097+
string code = """
2098+
using System;
2099+
using System.Collections;
2100+
using System.Collections.Generic;
2101+
using Microsoft.VisualStudio.TestTools.UnitTesting;
2102+
2103+
internal sealed class MyCustomCollection<T> : IEnumerable<T>
2104+
{
2105+
public IEnumerator<T> GetEnumerator()
2106+
{
2107+
throw new NotImplementedException();
2108+
}
2109+
2110+
IEnumerator IEnumerable.GetEnumerator()
2111+
{
2112+
throw new NotImplementedException();
2113+
}
2114+
2115+
public int Count => 5;
2116+
}
2117+
2118+
[TestClass]
2119+
public class MyTestClass
2120+
{
2121+
[TestMethod]
2122+
public void MyTestMethod()
2123+
{
2124+
var x = new MyCustomCollection<string>();
2125+
Assert.AreEqual(4, x.Count);
2126+
}
2127+
}
2128+
""";
2129+
2130+
await VerifyCS.VerifyCodeFixAsync(code, code);
2131+
}
2132+
2133+
[TestMethod]
2134+
public async Task WhenAssertAreEqualWithCollectionCountUsingNonGenericCollection()
2135+
{
2136+
string code = """
2137+
using System;
2138+
using System.Collections;
2139+
using System.Collections.Generic;
2140+
using Microsoft.VisualStudio.TestTools.UnitTesting;
2141+
2142+
[TestClass]
2143+
public class MyTestClass
2144+
{
2145+
[TestMethod]
2146+
public void MyTestMethod()
2147+
{
2148+
var x = new Hashtable();
2149+
Assert.AreEqual(4, x.Count);
2150+
// error CS0411: The type arguments for method 'Assert.HasCount<T>(int, IEnumerable<T>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
2151+
// When we add a non-generic IEnumerable overload, this test will fail because CS0411 is no longer reported.
2152+
// In that case, the analyzer should start reporting a diagnostic for the AreEqual call above.
2153+
// The codefix should suggest to switch to HasCount.
2154+
// Tracking issue https://github.com/microsoft/testfx/issues/6184.
2155+
Assert.{|CS0411:HasCount|}(4, x);
2156+
}
2157+
}
2158+
""";
2159+
2160+
await VerifyCS.VerifyCodeFixAsync(code, code);
2161+
}
20942162
#endregion
20952163
}

0 commit comments

Comments
 (0)