Skip to content

Commit

Permalink
Add support for IClassFixture<> on the collection definition
Browse files Browse the repository at this point in the history
  • Loading branch information
bradwilson committed Dec 3, 2023
1 parent 0bb00ae commit ce9c7fb
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,25 @@ [Fact] public void TestMethod() {{ }}
await Verify.VerifyAnalyzer(source);
}

[Fact]
public async void ClassFixtureOnCollectionDefinition_DoesNotTrigger()
{
var source = @"
using Xunit;
[CollectionDefinition(nameof(TestCollection))]
public class TestCollection : IClassFixture<object> { }
[Collection(nameof(TestCollection))]
public class TestClass {
public TestClass(object _) { }
[Fact] public void TestMethod() { }
}";

await Verify.VerifyAnalyzer(source);
}

[Fact]
public async void MissingClassFixtureDefinition_Triggers()
{
Expand Down
8 changes: 8 additions & 0 deletions src/xunit.analyzers/Utility/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ static partial class EnumerableExtensions
{
static readonly Func<object, bool> notNullTest = x => x is not null;

public static void AddRange<T>(
this HashSet<T> hashSet,
IEnumerable<T> source)
{
foreach (var item in source)
hashSet.Add(item);
}

/// <summary>
/// Returns <paramref name="source"/> as an enumerable of <typeparamref name="T"/> with
/// all the <c>null</c> items removed.
Expand Down
79 changes: 36 additions & 43 deletions src/xunit.analyzers/X1000/EnsureFixturesHaveASource.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -43,17 +43,24 @@ public override void AnalyzeCompilation(
.FirstOrDefault(a => a.AttributeClass.IsAssignableFrom(collectionAttributeType))
?.ConstructorArguments.FirstOrDefault().Value?.ToString();
// Determine which constructor arguments will come from IClassFixture<>
var classFixtureInterfaceType = xunitContext.Core.IClassFixtureType?.ConstructUnboundGenericType();
var classFixtureTypes =
// Need to construct a full set of types we know can be resolved. Start with things
// like ITestOutputHelper and ITestContextAccessor (since they're injected by the framework)
var validConstructorArgumentTypes = new HashSet<ITypeSymbol?>(SymbolEqualityComparer.Default)
{
xunitContext.Core.ITestOutputHelperType,
xunitContext.V3Core?.ITestContextAccessorType
};
// Add types from IClassFixture<> on the class
var classFixtureType = xunitContext.Core.IClassFixtureType?.ConstructUnboundGenericType();
validConstructorArgumentTypes.AddRange(
namedType
.AllInterfaces
.Where(i => i.IsGenericType && SymbolEqualityComparer.Default.Equals(classFixtureInterfaceType, i.ConstructUnboundGenericType()))
.Select(i => i.TypeArguments.First() as INamedTypeSymbol)
.WhereNotNull()
.ToImmutableHashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
.Where(i => i.IsGenericType && SymbolEqualityComparer.Default.Equals(classFixtureType, i.ConstructUnboundGenericType()))
.Select(i => i.TypeArguments.First())
);
// Determine which constructor arguments will come from ICollectionFixture<> on a fixture definition
// Add types from IClassFixture<> and ICollectionFixture<> on the collection definition
var collectionFixtureTypes = ImmutableHashSet<INamedTypeSymbol>.Empty;
if (collectionDefinitionName != null)
{
Expand All @@ -69,50 +76,36 @@ bool MatchCollectionDefinition(INamedTypeSymbol symbol) =>
var matchingType = namedType.ContainingAssembly.FindNamedType(MatchCollectionDefinition);
if (matchingType is not null)
{
var collectionFixtureType = xunitContext.Core.ICollectionFixtureType;
collectionFixtureTypes =
matchingType
.AllInterfaces
.Where(i => i.OriginalDefinition.IsAssignableFrom(collectionFixtureType))
.Select(i => i.TypeArguments.FirstOrDefault() as INamedTypeSymbol)
.WhereNotNull()
.ToImmutableHashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
var collectionFixtureType = xunitContext.Core.ICollectionFixtureType?.ConstructUnboundGenericType();
foreach (var @interface in matchingType.AllInterfaces.Where(i => i.IsGenericType))
{
var unboundGeneric = @interface.ConstructUnboundGenericType();
if (SymbolEqualityComparer.Default.Equals(classFixtureType, unboundGeneric)
|| SymbolEqualityComparer.Default.Equals(collectionFixtureType, unboundGeneric))
validConstructorArgumentTypes.Add(@interface.TypeArguments.First());
}
}
}
// Determine which constructor arguments will come from IAssemblyFixture
var assemblyFixtureTypes = ImmutableHashSet<INamedTypeSymbol>.Empty;
// Add types from AssemblyFixtureAttribute on the assembly
var assemblyFixtureAttributeType = xunitContext.V3Core?.AssemblyFixtureAttributeType;
if (assemblyFixtureAttributeType is not null)
assemblyFixtureTypes =
validConstructorArgumentTypes.AddRange(
namedType
.ContainingAssembly
.GetAttributes()
.Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, assemblyFixtureAttributeType))
.Select(a => a.ConstructorArguments[0].Value as INamedTypeSymbol)
.WhereNotNull()
.ToImmutableHashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
// Exclude things like ITestOutputHelper and ITestContextAccessor
var supportedNonFixtureTypes =
new[] { xunitContext.Core.ITestOutputHelperType, xunitContext.V3Core?.ITestContextAccessorType }
.WhereNotNull()
.ToImmutableHashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
.Select(a => a.ConstructorArguments[0].Value as ITypeSymbol)
);
foreach (var parameter in ctors[0].Parameters)
if (!supportedNonFixtureTypes.Contains(parameter.Type, SymbolEqualityComparer.Default)
&& !classFixtureTypes.Contains(parameter.Type, SymbolEqualityComparer.Default)
&& !collectionFixtureTypes.Contains(parameter.Type, SymbolEqualityComparer.Default)
&& !assemblyFixtureTypes.Contains(parameter.Type, SymbolEqualityComparer.Default))
{
context.ReportDiagnostic(
Diagnostic.Create(
Descriptors.X1041_EnsureFixturesHaveASource,
parameter.Locations.FirstOrDefault(),
parameter.Name
)
);
}
foreach (var parameter in ctors[0].Parameters.Where(p => !validConstructorArgumentTypes.Contains(p.Type)))
context.ReportDiagnostic(
Diagnostic.Create(
Descriptors.X1041_EnsureFixturesHaveASource,
parameter.Locations.FirstOrDefault(),
parameter.Name
)
);
}, SymbolKind.NamedType);
}
}

0 comments on commit ce9c7fb

Please sign in to comment.