diff --git a/src/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/DataSourceAttributeHelper.cs b/src/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/DataSourceAttributeHelper.cs index 729f89dfb2b..c326bf83cde 100644 --- a/src/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/DataSourceAttributeHelper.cs +++ b/src/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/DataSourceAttributeHelper.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using TUnit.Core.SourceGenerator.Helpers; +using TUnit.Core.SourceGenerator.Extensions; namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers; @@ -12,8 +13,32 @@ public static bool IsDataSourceAttribute(INamedTypeSymbol? attributeClass) return false; } - // Check if the attribute implements IDataSourceAttribute - return InterfaceHelper.ImplementsInterface(attributeClass, "global::TUnit.Core.IDataSourceAttribute"); + foreach (var implementedInterface in attributeClass.AllInterfaces) + { + if (implementedInterface.Name != "IDataSourceAttribute") + { + continue; + } + + // Match the usual interface without allocating its fully qualified display name. + if (implementedInterface.Arity == 0 && implementedInterface.ContainingType == null && + implementedInterface.ContainingNamespace is + { + Name: "Core", + ContainingNamespace: { Name: "TUnit", ContainingNamespace.IsGlobalNamespace: true } + }) + { + return true; + } + + // Preserve the existing display-name matching for unusual nested or generic symbols. + if (implementedInterface.GloballyQualified() == "global::TUnit.Core.IDataSourceAttribute") + { + return true; + } + } + + return false; } public static bool IsTypedDataSourceAttribute(INamedTypeSymbol? attributeClass) @@ -38,4 +63,4 @@ public static bool IsTypedDataSourceAttribute(INamedTypeSymbol? attributeClass) return typedInterface?.TypeArguments.FirstOrDefault(); } -} \ No newline at end of file +} diff --git a/src/TUnit.Core.SourceGenerator/Extensions/AttributeDataExtensions.cs b/src/TUnit.Core.SourceGenerator/Extensions/AttributeDataExtensions.cs index 282b1a43693..36cad840b4d 100644 --- a/src/TUnit.Core.SourceGenerator/Extensions/AttributeDataExtensions.cs +++ b/src/TUnit.Core.SourceGenerator/Extensions/AttributeDataExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using TUnit.Core.SourceGenerator.Helpers; +using TUnit.Core.SourceGenerator.CodeGenerators.Helpers; namespace TUnit.Core.SourceGenerator.Extensions; @@ -17,13 +18,7 @@ public static bool IsTestAttribute(this AttributeData? attributeData) public static bool IsDataSourceAttribute(this AttributeData? attributeData) { - if (attributeData?.AttributeClass == null) - { - return false; - } - - return InterfaceHelper.ImplementsInterface(attributeData.AttributeClass, - WellKnownFullyQualifiedClassNames.IDataSourceAttribute.WithGlobalPrefix); + return DataSourceAttributeHelper.IsDataSourceAttribute(attributeData?.AttributeClass); } public static bool IsTypedDataSourceAttribute(this AttributeData? attributeData) diff --git a/tests/TUnit.Core.SourceGenerator.Tests/DataSourceAttributeDetectionTests.cs b/tests/TUnit.Core.SourceGenerator.Tests/DataSourceAttributeDetectionTests.cs new file mode 100644 index 00000000000..eca88c57467 --- /dev/null +++ b/tests/TUnit.Core.SourceGenerator.Tests/DataSourceAttributeDetectionTests.cs @@ -0,0 +1,81 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TUnit.Core.SourceGenerator.Extensions; + +namespace TUnit.Core.SourceGenerator.Tests; + +public class DataSourceAttributeDetectionTests +{ + [Test] + [Arguments("DirectSource", true)] + [Arguments("InheritedSource", true)] + [Arguments("IndirectSource", true)] + [Arguments("GenericSource", true)] + [Arguments("Unrelated", false)] + [Arguments("OtherNamespace", false)] + [Arguments("PrefixedNamespace", false)] + [Arguments("GenericInterface", false)] + [Arguments("NestedInterface", false)] + public async Task RecognizesDataSourceInterfaces(string attributeName, bool expected) + { + var source = """ + namespace TUnit.Core + { + public interface IDataSourceAttribute { } + public interface IDataSourceAttribute { } + public interface IIndirect : IDataSourceAttribute { } + } + namespace Other { public interface IDataSourceAttribute { } } + namespace Other.TUnit.Core { public interface IDataSourceAttribute { } } + public class Container { public interface IDataSourceAttribute { } } + public class DirectSource : System.Attribute, TUnit.Core.IDataSourceAttribute { } + public class InheritedSource : DirectSource { } + public class IndirectSource : System.Attribute, TUnit.Core.IIndirect { } + public class GenericSource : System.Attribute, TUnit.Core.IDataSourceAttribute { } + public class Unrelated : System.Attribute, System.ICloneable + { + public object Clone() => this; + } + public class OtherNamespace : System.Attribute, Other.IDataSourceAttribute { } + public class PrefixedNamespace : System.Attribute, Other.TUnit.Core.IDataSourceAttribute { } + public class GenericInterface : System.Attribute, TUnit.Core.IDataSourceAttribute { } + public class NestedInterface : System.Attribute, Container.IDataSourceAttribute { } + """ + $"\n[{attributeName}] public class Subject {{ }}"; + + var attribute = await GetSubjectAttribute(source); + await Assert.That(attribute.IsDataSourceAttribute()).IsEqualTo(expected); + } + + [Test] + public async Task NestedInterfaceWithMatchingDisplayName_PreservesExistingBehavior() + { + // Display-name matching historically accepts this spelling even though Core is a type. + const string source = """ + namespace TUnit + { + public class Core { public interface IDataSourceAttribute { } } + } + public class Source : System.Attribute, TUnit.Core.IDataSourceAttribute { } + [Source] public class Subject { } + """; + + var attribute = await GetSubjectAttribute(source); + await Assert.That(attribute.IsDataSourceAttribute()).IsTrue(); + } + + [Test] + public async Task NullAttribute_IsNotDataSource() + { + await Assert.That(AttributeDataExtensions.IsDataSourceAttribute(null)).IsFalse(); + } + + private static async Task GetSubjectAttribute(string source) + { + var compilation = CSharpCompilation.Create("AttributeDetection", + [CSharpSyntaxTree.ParseText(source)], ReferencesHelper.References, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + await Assert.That(compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error)).IsEmpty(); + return compilation.GetTypeByMetadataName("Subject")!.GetAttributes().Single(); + } +}