Skip to content

Commit

Permalink
xunit/xunit#2680: Add Analyzer to warn about calling Assert.Empty wit…
Browse files Browse the repository at this point in the history
…h StringValues or ArraySegment<T>
  • Loading branch information
bradwilson committed Jan 7, 2024
1 parent 179e874 commit 01f4b5f
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Xunit;
using Verify = CSharpVerifier<Xunit.Analyzers.DoNotUseAssertEmptyWithProblematicTypes>;

public class DoNotUseAssertEmptyWithProblematicTypes
{
public static TheoryData<string, string, string> ProblematicTypes = new()
{
{ "StringValues.Empty", "StringValues", "it is implicitly cast to a string, not a collection" },
{ "new ArraySegment<int>()", "ArraySegment<int>", "its implementation of GetEnumerator() can throw" },
};

[Theory]
[InlineData("new int[0]")]
[InlineData("new List<int>()")]
[InlineData("new Dictionary<string, int>()")]
public async Task NonProblematicCollection_DoesNotTrigger(string invocation)
{
var source = @$"
using System;
using System.Collections.Generic;
using Xunit;
public class TestClass {{
public void TestMethod() {{
Assert.Empty({invocation});
}}
}}";

await Verify.VerifyAnalyzer(source);
}

[Theory]
[MemberData(nameof(ProblematicTypes))]
public async Task ConvertingToCollection_DoesNotTrigger(
string invocation,
string _1,
string _2)
{
var source = @$"
using System;
using System.Linq;
using Microsoft.Extensions.Primitives;
using Xunit;
public class TestClass {{
public void TestMethod() {{
Assert.Empty({invocation}.ToArray());
}}
}}";

await Verify.VerifyAnalyzer(source);
}

[Theory]
[MemberData(nameof(ProblematicTypes))]
public async Task UsingProblematicType_Triggers(
string invocation,
string typeName,
string problem)
{
var source = @$"
using System;
using Microsoft.Extensions.Primitives;
using Xunit;
public class TestClass {{
public void TestMethod() {{
Assert.Empty({invocation});
}}
}}";

var expected =
Verify
.Diagnostic()
.WithSpan(8, 9, 8, 23 + invocation.Length)
.WithSeverity(DiagnosticSeverity.Warning)
.WithArguments(typeName, problem);

await Verify.VerifyAnalyzer(source, expected);
}
}
9 changes: 8 additions & 1 deletion src/xunit.analyzers/Utility/Descriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,14 @@ static DiagnosticDescriptor Rule(
"Comparing an instance of {0} with an instance of {1} has undefined results, because the order of items in the set is not predictable. Create a stable order for the set (i.e., by using OrderBy from Linq)."
);

// Placeholder for rule X2028
public static DiagnosticDescriptor X2028_DoNotUseAssertEmptyWithProblematicTypes { get; } =
Rule(
"xUnit2028",
"Do not use Assert.Empty with problematic types",
Assertions,
Warning,
"Using Assert.Empty with an instance of {0} is problematic, because {1}. Check the length with .Count instead."
);

// Placeholder for rule X2029

Expand Down
9 changes: 9 additions & 0 deletions src/xunit.analyzers/Utility/TypeSymbolFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ namespace Xunit.Analyzers;

public static class TypeSymbolFactory
{
public static INamedTypeSymbol? ArraySegmentOfT(Compilation compilation) =>
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("System.ArraySegment`1");

public static INamedTypeSymbol? AssemblyFixtureAttribute_V3(Compilation compilation) =>
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("Xunit.AssemblyFixtureAttribute");

Expand Down Expand Up @@ -187,6 +190,12 @@ public static IArrayTypeSymbol ObjectArray(Compilation compilation) =>
public static INamedTypeSymbol? SortedSetOfT(Compilation compilation) =>
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("System.Collections.Generic.SortedSet`1");

public static INamedTypeSymbol String(Compilation compilation) =>
Guard.ArgumentNotNull(compilation).GetSpecialType(SpecialType.System_String);

public static INamedTypeSymbol? StringValues(Compilation compilation) =>
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("Microsoft.Extensions.Primitives.StringValues");

public static INamedTypeSymbol? Task(Compilation compilation) =>
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("System.Threading.Tasks.Task");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Xunit.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class DoNotUseAssertEmptyWithProblematicTypes : AssertUsageAnalyzerBase
{
public DoNotUseAssertEmptyWithProblematicTypes() :
base(Descriptors.X2028_DoNotUseAssertEmptyWithProblematicTypes, new[] { Constants.Asserts.Empty })
{ }

protected override void AnalyzeInvocation(
OperationAnalysisContext context,
XunitContext xunitContext,
IInvocationOperation invocationOperation,
IMethodSymbol method)
{
Guard.ArgumentNotNull(xunitContext);
Guard.ArgumentNotNull(invocationOperation);
Guard.ArgumentNotNull(method);

var semanticModel = context.Operation.SemanticModel;
if (semanticModel is null)
return;

var arguments = invocationOperation.Arguments;
if (arguments.Length != 1)
return;

if (method.Parameters.Length != 1)
return;

if (semanticModel.GetTypeInfo(arguments[0].Value.Syntax).Type is not INamedTypeSymbol sourceType)
return;

var stringValuesType = TypeSymbolFactory.StringValues(context.Compilation);
if (stringValuesType is not null && SymbolEqualityComparer.Default.Equals(sourceType, stringValuesType))
context.ReportDiagnostic(
Diagnostic.Create(
Descriptors.X2028_DoNotUseAssertEmptyWithProblematicTypes,
invocationOperation.Syntax.GetLocation(),
sourceType.ToMinimalDisplayString(semanticModel, 0),
"it is implicitly cast to a string, not a collection"
)
);

if (sourceType.IsGenericType)
{
var arraySegmentType = TypeSymbolFactory.ArraySegmentOfT(context.Compilation)?.ConstructUnboundGenericType();
if (arraySegmentType is not null && SymbolEqualityComparer.Default.Equals(sourceType.ConstructUnboundGenericType(), arraySegmentType))
context.ReportDiagnostic(
Diagnostic.Create(
Descriptors.X2028_DoNotUseAssertEmptyWithProblematicTypes,
invocationOperation.Syntax.GetLocation(),
sourceType.ToMinimalDisplayString(semanticModel, 0),
"its implementation of GetEnumerator() can throw"
)
);
}
}
}

0 comments on commit 01f4b5f

Please sign in to comment.