Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.CodeAnalysis;
using TUnit.Core.SourceGenerator.Helpers;
using TUnit.Core.SourceGenerator.Extensions;

namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers;

Expand All @@ -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)
Expand All @@ -38,4 +63,4 @@ public static bool IsTypedDataSourceAttribute(INamedTypeSymbol? attributeClass)

return typedInterface?.TypeArguments.FirstOrDefault();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.CodeAnalysis;
using TUnit.Core.SourceGenerator.Helpers;
using TUnit.Core.SourceGenerator.CodeGenerators.Helpers;

namespace TUnit.Core.SourceGenerator.Extensions;

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<int>", 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<T> { }
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<T> : 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<int> { }
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<AttributeData> 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();
}
}
Loading