Skip to content

Commit

Permalink
Detect when CollectionDefinition class is not in same assembly as tes…
Browse files Browse the repository at this point in the history
…t class (xunit/xunit#2311) (#169)
  • Loading branch information
etherfield authored Nov 27, 2023
1 parent 45a493e commit 81de769
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Microsoft.CodeAnalysis;
using Xunit;
using Verify = CSharpVerifier<Xunit.Analyzers.CollectionDefinitionMustBeInTheSameAssembly>;

public class CollectionDefinitionMustBeInTheSameAssemblyTests
{
public static TheoryData<string, string> NoDiagnosticsCases = new()
{
{
"[Collection(\"Test collection definition\")]",
"[CollectionDefinition(\"Test collection definition\")]"
},
{
string.Empty,
"[CollectionDefinition(\"Test collection definition\")]"
},
{
string.Empty,
string.Empty
}
};

static readonly string Template = @"
using Xunit;
{0}
public class TestClass {{ }}
namespace TestNamespace {{
{1}
public class TestDefinition {{ }}
}}";

[Theory]
[MemberData(nameof(NoDiagnosticsCases))]
public async void CollectionDefinitionIsPresentInTheAssembly_NoDiagnostics(string classAttribute, string definitionAttribute)
{
var source = string.Format(Template, classAttribute, definitionAttribute);

await Verify.VerifyAnalyzer(source);
}

[Fact]
public async void CollectionDefinitionIsMissingInTheAssembly_ReturnsError()
{
var source = string.Format(Template, "[Collection(\"Test collection definition\")]", string.Empty);

var expected =
Verify
.Diagnostic()
.WithSpan(5, 14, 5, 23)
.WithSeverity(DiagnosticSeverity.Error)
.WithArguments("Test collection definition", "TestProject");

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 @@ -404,7 +404,14 @@ static DiagnosticDescriptor Rule(
"The type argument {0} from {1}.{2} is nullable, while the type of the corresponding test method parameter {3} is not. Make the TheoryData type non-nullable, or make the test method parameter nullable."
);

// Placeholder for rule X1041
public static DiagnosticDescriptor X1041_CollectionDefinitionMustBeInTheSameAssembly { get; } =
Rule(
"xUnit1041",
"Collection definitions must be in the same assembly as the test that uses them",
Extensibility,
Error,
"A class for '{0}' collection definition must be declared in the '{1}' assembly"
);

// Placeholder for rule X1042

Expand Down
2 changes: 2 additions & 0 deletions src/xunit.analyzers/Utility/EmptyCoreContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public class EmptyCoreContext : ICoreContext

public INamedTypeSymbol? ClassDataAttributeType => null;

public INamedTypeSymbol? CollectionAttributeType => null;

public INamedTypeSymbol? CollectionDefinitionAttributeType => null;

public INamedTypeSymbol? DataAttributeType => null;
Expand Down
2 changes: 2 additions & 0 deletions src/xunit.analyzers/Utility/ICoreContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ public interface ICoreContext
{
INamedTypeSymbol? ClassDataAttributeType { get; }

INamedTypeSymbol? CollectionAttributeType { get; }

INamedTypeSymbol? CollectionDefinitionAttributeType { get; }

INamedTypeSymbol? DataAttributeType { get; }
Expand Down
60 changes: 60 additions & 0 deletions src/xunit.analyzers/Utility/SymbolAssemblyVisitor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Linq;
using Microsoft.CodeAnalysis;

namespace Xunit.Analyzers;

/// <summary>
/// Visits every type in every namespace within an assembly. Expressions are provided which indicate
/// whether the item being searched for has been found. Searches stop once at least one of the
/// short circuit expressions returned true.
/// </summary>
public class SymbolAssemblyVisitor : SymbolVisitor
{
readonly Func<INamedTypeSymbol, bool>[] shortCircuitExpressions;

public SymbolAssemblyVisitor(params Func<INamedTypeSymbol, bool>[] shortCircuitExpressions)
{
this.shortCircuitExpressions = shortCircuitExpressions;
}

public bool ShortCircuitTriggered { get; private set; }

public override void VisitAssembly(IAssemblySymbol symbol)
{
symbol.GlobalNamespace.Accept(this);
}

public override void VisitNamespace(INamespaceSymbol symbol)
{
var namespaceOrTypes = symbol.GetMembers();
foreach (var member in namespaceOrTypes)
{
if (ShortCircuitTriggered)
return;

member.Accept(this);
}
}

public override void VisitNamedType(INamedTypeSymbol symbol)
{
if (shortCircuitExpressions.Any(e => e(symbol)))
{
ShortCircuitTriggered = true;
return;
}

var nestedTypes = symbol.GetTypeMembers();
if (nestedTypes.IsDefaultOrEmpty)
return;

foreach (var nestedType in nestedTypes)
{
if (ShortCircuitTriggered)
return;

nestedType.Accept(this);
}
}
}
3 changes: 3 additions & 0 deletions src/xunit.analyzers/Utility/TypeSymbolFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ public class TypeSymbolFactory
public static INamedTypeSymbol? ClassDataAttribute(Compilation compilation) =>
compilation.GetTypeByMetadataName("Xunit.ClassDataAttribute");

public static INamedTypeSymbol? CollectionAttribute(Compilation compilation) =>
compilation.GetTypeByMetadataName("Xunit.CollectionAttribute");

public static INamedTypeSymbol? CollectionDefinitionAttribute(Compilation compilation) =>
compilation.GetTypeByMetadataName("Xunit.CollectionDefinitionAttribute");

Expand Down
5 changes: 5 additions & 0 deletions src/xunit.analyzers/Utility/V2CoreContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class V2CoreContext : ICoreContext
internal static readonly Version Version_2_4_0 = new("2.4.0");

readonly Lazy<INamedTypeSymbol?> lazyClassDataAttributeType;
readonly Lazy<INamedTypeSymbol?> lazyCollectionAttributeType;
readonly Lazy<INamedTypeSymbol?> lazyCollectionDefinitionAttributeType;
readonly Lazy<INamedTypeSymbol?> lazyDataAttributeType;
readonly Lazy<INamedTypeSymbol?> lazyFactAttributeType;
Expand All @@ -26,6 +27,7 @@ public class V2CoreContext : ICoreContext
Version = version;

lazyClassDataAttributeType = new(() => TypeSymbolFactory.ClassDataAttribute(compilation));
lazyCollectionAttributeType = new(() => TypeSymbolFactory.CollectionAttribute(compilation));
lazyCollectionDefinitionAttributeType = new(() => TypeSymbolFactory.CollectionDefinitionAttribute(compilation));
lazyDataAttributeType = new(() => TypeSymbolFactory.DataAttribute(compilation));
lazyFactAttributeType = new(() => TypeSymbolFactory.FactAttribute(compilation));
Expand All @@ -39,6 +41,9 @@ public class V2CoreContext : ICoreContext
public INamedTypeSymbol? ClassDataAttributeType =>
lazyClassDataAttributeType.Value;

public INamedTypeSymbol? CollectionAttributeType =>
lazyCollectionAttributeType.Value;

public INamedTypeSymbol? CollectionDefinitionAttributeType =>
lazyCollectionDefinitionAttributeType.Value;

Expand Down
5 changes: 5 additions & 0 deletions src/xunit.analyzers/Utility/V3CoreContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Xunit.Analyzers;
public class V3CoreContext : ICoreContext
{
readonly Lazy<INamedTypeSymbol?> lazyClassDataAttributeType;
readonly Lazy<INamedTypeSymbol?> lazyCollectionAttributeType;
readonly Lazy<INamedTypeSymbol?> lazyCollectionDefinitionAttributeType;
readonly Lazy<INamedTypeSymbol?> lazyDataAttributeType;
readonly Lazy<INamedTypeSymbol?> lazyFactAttributeType;
Expand All @@ -23,6 +24,7 @@ public class V3CoreContext : ICoreContext
Version = version;

lazyClassDataAttributeType = new(() => TypeSymbolFactory.ClassDataAttribute(compilation));
lazyCollectionAttributeType = new(() => TypeSymbolFactory.CollectionAttribute(compilation));
lazyCollectionDefinitionAttributeType = new(() => TypeSymbolFactory.CollectionDefinitionAttribute(compilation));
lazyDataAttributeType = new(() => TypeSymbolFactory.DataAttribute(compilation));
lazyFactAttributeType = new(() => TypeSymbolFactory.FactAttribute(compilation));
Expand All @@ -36,6 +38,9 @@ public class V3CoreContext : ICoreContext
public INamedTypeSymbol? ClassDataAttributeType =>
lazyClassDataAttributeType.Value;

public INamedTypeSymbol? CollectionAttributeType =>
lazyCollectionAttributeType.Value;

public INamedTypeSymbol? CollectionDefinitionAttributeType =>
lazyCollectionDefinitionAttributeType.Value;

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

namespace Xunit.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CollectionDefinitionMustBeInTheSameAssembly : XunitDiagnosticAnalyzer
{
public CollectionDefinitionMustBeInTheSameAssembly() :
base(Descriptors.X1041_CollectionDefinitionMustBeInTheSameAssembly)
{ }

public override void AnalyzeCompilation(
CompilationStartAnalysisContext context,
XunitContext xunitContext)
{
context.RegisterSymbolAction(context =>
{
if (context.Symbol is not INamedTypeSymbol namedType)
return;
var collectionAttributeType = xunitContext.Core.CollectionAttributeType;
var collectionAttribute = namedType
.GetAttributes()
.FirstOrDefault(a => a.AttributeClass.IsAssignableFrom(collectionAttributeType));
var collectionDefinitionName = collectionAttribute?.ConstructorArguments[0].Value?.ToString();
if (collectionDefinitionName == null)
return;
var collectionDefinitionAttributeType = xunitContext.Core.CollectionDefinitionAttributeType;
var visitor = new SymbolAssemblyVisitor(symbol =>
symbol
.GetAttributes()
.Any(a =>
a.AttributeClass.IsAssignableFrom(collectionDefinitionAttributeType) &&
!a.ConstructorArguments.IsDefaultOrEmpty &&
a.ConstructorArguments[0].Value?.ToString() == collectionDefinitionName
)
);
var currentAssembly = context.Compilation.Assembly;
visitor.Visit(currentAssembly);
if (visitor.ShortCircuitTriggered)
return;
context.ReportDiagnostic(
Diagnostic.Create(
Descriptors.X1041_CollectionDefinitionMustBeInTheSameAssembly,
namedType.Locations.First(),
collectionDefinitionName,
currentAssembly.Name
)
);
}, SymbolKind.NamedType);
}
}

0 comments on commit 81de769

Please sign in to comment.