Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;

namespace TUnit.Core.SourceGenerator.Generators;

/// <summary>
/// Declares an attribute that generated code relies on as an internal type, but only when the project has no
/// single usable declaration of it: it is missing (.NET Framework, .NET Standard) or declared by several
/// references so that it is ambiguous.
/// </summary>
/// <remarks>
/// The existence check needs the compilation, so this cannot use RegisterPostInitializationOutput.
/// Types from other source generators are invisible here, so PolySharp (the common generator-based
/// polyfill) is detected through the build properties it makes visible to the compiler instead.
/// Set <c>EnableTUnitPolyfills</c> to <c>false</c> to turn the fallback off.
/// </remarks>
public abstract class AttributePolyfillGenerator : IIncrementalGenerator
{
/// <summary>The attribute's metadata name, e.g. <c>System.Runtime.CompilerServices.ModuleInitializerAttribute</c>.</summary>
protected abstract string AttributeMetadataName { get; }

protected abstract string HintName { get; }

protected abstract string Source { get; }

public void Initialize(IncrementalGeneratorInitializationContext context)
{
var metadataName = AttributeMetadataName;
var hintName = HintName;
var source = Source;

var allowedProvider = context.AnalyzerConfigOptionsProvider
.Select((options, _) => IsAllowed(options.GlobalOptions, metadataName));

var attributeMissingProvider = context.CompilationProvider
.Select((compilation, _) => !HasAccessibleAttribute(compilation, metadataName));

context.RegisterSourceOutput(attributeMissingProvider.Combine(allowedProvider), (spc, data) =>
{
if (data.Left && data.Right)
{
spc.AddSource(hintName, SourceText.From(source, System.Text.Encoding.UTF8));
}
});
}

private static bool IsAllowed(AnalyzerConfigOptions options, string metadataName)
{
if (IsFalse(options, "build_property.EnableTUnitSourceGeneration")
|| IsFalse(options, "build_property.EnableTUnitPolyfills"))
{
return false;
}

return !PolySharpProvidesAttribute(options, metadataName);
}

private static bool IsFalse(AnalyzerConfigOptions options, string key)
{
return options.TryGetValue(key, out var value)
&& string.Equals(value.Trim(), "false", StringComparison.OrdinalIgnoreCase);
}

// PolySharp marks its settings as compiler-visible, so the keys exist (possibly empty) whenever it is installed.
// It generates every polyfill that is missing unless the type is filtered out by these two lists.
private static bool PolySharpProvidesAttribute(AnalyzerConfigOptions options, string metadataName)
{
if (!options.TryGetValue("build_property.PolySharpIncludeGeneratedTypes", out var included))
{
return false;
}

if (!string.IsNullOrWhiteSpace(included) && !ListContainsAttribute(included, metadataName))
{
return false;
}

return !options.TryGetValue("build_property.PolySharpExcludeGeneratedTypes", out var excluded)
|| !ListContainsAttribute(excluded, metadataName);
}

private static bool ListContainsAttribute(string list, string metadataName)
{
foreach (var entry in list.Split([';', ','], StringSplitOptions.RemoveEmptyEntries))
{
if (string.Equals(entry.Trim(), metadataName, StringComparison.Ordinal))
{
return true;
}
}

return false;
}

private static bool HasAccessibleAttribute(Compilation compilation, string metadataName)
{
// GetTypeByMetadataName returns null when several references declare the type (common with
// polyfill packages), so inspect every candidate. The compiler ignores [Embedded] types from other
// assemblies, which is how Polyfill declares them, even when InternalsVisibleTo makes them accessible.
INamedTypeSymbol? accessible = null;
var accessibleCount = 0;

foreach (var type in compilation.GetTypesByMetadataName(metadataName))
{
if (compilation.IsSymbolAccessibleWithin(type, compilation.Assembly)
&& !IsEmbeddedFromReference(type, compilation))
{
accessible = type;
accessibleCount++;
}
}

if (accessibleCount <= 1)
{
return accessible != null;
}

// Several accessible definitions are not always ambiguous (a declaration in source wins, for example).
// Only the compiler's own lookup gives the right answer, so bind the name the generated code uses and
// declare our own attribute only when that fails (CS0433). Ours is then in source, so it takes precedence
// over the referenced ones.
var tree = compilation.SyntaxTrees.FirstOrDefault();

if (tree is null)
{
// Speculative binding needs a tree, and other generators may still emit code that uses the attribute.
tree = CSharpSyntaxTree.ParseText(string.Empty);
compilation = compilation.AddSyntaxTrees(tree);
}

var typeInfo = compilation.GetSemanticModel(tree).GetSpeculativeTypeInfo(
0,
SyntaxFactory.ParseTypeName("global::" + metadataName),
SpeculativeBindingOption.BindAsTypeOrNamespace);

return typeInfo.Type is { TypeKind: not TypeKind.Error };
}

private static bool IsEmbeddedFromReference(INamedTypeSymbol type, Compilation compilation)
{
if (SymbolEqualityComparer.Default.Equals(type.ContainingAssembly, compilation.Assembly))
{
return false;
}

foreach (var attribute in type.GetAttributes())
{
if (attribute.AttributeClass is { Name: "EmbeddedAttribute", ContainingNamespace: { Name: "CodeAnalysis", ContainingNamespace: { Name: "Microsoft", ContainingNamespace.IsGlobalNamespace: true } } })
{
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Microsoft.CodeAnalysis;

namespace TUnit.Core.SourceGenerator.Generators;

/// <summary>
/// Generated code keeps each non-public test method available to reflection under trimming with
/// <c>[DynamicDependency]</c>, but <c>System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute</c> only ships
/// in .NET 5+. A .NET Standard test library can still end up in a trimmed or Native AOT application, and the
/// trimmer recognises the attribute by name in any assembly, so this generator declares it as an internal type
/// when nothing else provides it.
/// </summary>
[Generator]
public sealed class DynamicDependencyPolyfillGenerator : AttributePolyfillGenerator
{
protected override string AttributeMetadataName => "System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute";

protected override string HintName => "TUnit.DynamicDependencyAttribute.g.cs";

protected override string Source =>
"""
// <auto-generated/>
// Declared by TUnit because the project has no single usable DynamicDependencyAttribute: it is missing
// (.NET Framework, .NET Standard) or declared by several references. The trimmer matches the attribute
// by name, so this declaration works for trimmed and Native AOT applications. Set the
// EnableTUnitPolyfills MSBuild property to false to opt out.
#nullable enable
namespace System.Diagnostics.CodeAnalysis
{
[global::System.AttributeUsage(global::System.AttributeTargets.Constructor | global::System.AttributeTargets.Field | global::System.AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal sealed class DynamicDependencyAttribute : global::System.Attribute
{
public DynamicDependencyAttribute(string memberSignature)
{
MemberSignature = memberSignature;
}

public DynamicDependencyAttribute(string memberSignature, global::System.Type type)
{
MemberSignature = memberSignature;
Type = type;
}

public DynamicDependencyAttribute(string memberSignature, string typeName, string assemblyName)
{
MemberSignature = memberSignature;
TypeName = typeName;
AssemblyName = assemblyName;
}

public string? MemberSignature { get; }

public global::System.Type? Type { get; }

public string? TypeName { get; }

public string? AssemblyName { get; }

public string? Condition { get; set; }
}
}
""";
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;

namespace TUnit.Core.SourceGenerator.Generators;

Expand All @@ -12,18 +9,14 @@ namespace TUnit.Core.SourceGenerator.Generators;
/// or several references declare it so that it is ambiguous, this generator declares it as an internal
/// type so the generated code compiles without the project having to reference a polyfill package.
/// </summary>
/// <remarks>
/// The existence check needs the compilation, so this cannot use RegisterPostInitializationOutput.
/// Types from other source generators are invisible here, so PolySharp (the common generator-based
/// polyfill) is detected through the build properties it makes visible to the compiler instead.
/// Set <c>EnableTUnitPolyfills</c> to <c>false</c> to turn the fallback off.
/// </remarks>
[Generator]
public sealed class ModuleInitializerPolyfillGenerator : IIncrementalGenerator
public sealed class ModuleInitializerPolyfillGenerator : AttributePolyfillGenerator
{
private const string AttributeMetadataName = "System.Runtime.CompilerServices.ModuleInitializerAttribute";
protected override string AttributeMetadataName => "System.Runtime.CompilerServices.ModuleInitializerAttribute";

private const string Source =
protected override string HintName => "TUnit.ModuleInitializerAttribute.g.cs";

protected override string Source =>
"""
// <auto-generated/>
// Declared by TUnit because the project has no single usable ModuleInitializerAttribute: it is missing
Expand All @@ -38,131 +31,4 @@ internal sealed class ModuleInitializerAttribute : global::System.Attribute
}
}
""";

public void Initialize(IncrementalGeneratorInitializationContext context)
{
var allowedProvider = context.AnalyzerConfigOptionsProvider
.Select(static (options, _) => IsAllowed(options.GlobalOptions));

var attributeMissingProvider = context.CompilationProvider
.Select(static (compilation, _) => !HasAccessibleAttribute(compilation));

context.RegisterSourceOutput(attributeMissingProvider.Combine(allowedProvider), static (spc, data) =>
{
if (data.Left && data.Right)
{
spc.AddSource("TUnit.ModuleInitializerAttribute.g.cs", SourceText.From(Source, System.Text.Encoding.UTF8));
}
});
}

private static bool IsAllowed(AnalyzerConfigOptions options)
{
if (IsFalse(options, "build_property.EnableTUnitSourceGeneration")
|| IsFalse(options, "build_property.EnableTUnitPolyfills"))
{
return false;
}

return !PolySharpProvidesAttribute(options);
}

private static bool IsFalse(AnalyzerConfigOptions options, string key)
{
return options.TryGetValue(key, out var value)
&& string.Equals(value.Trim(), "false", StringComparison.OrdinalIgnoreCase);
}

// PolySharp marks its settings as compiler-visible, so the keys exist (possibly empty) whenever it is installed.
// It generates every polyfill that is missing unless the type is filtered out by these two lists.
private static bool PolySharpProvidesAttribute(AnalyzerConfigOptions options)
{
if (!options.TryGetValue("build_property.PolySharpIncludeGeneratedTypes", out var included))
{
return false;
}

if (!string.IsNullOrWhiteSpace(included) && !ListContainsAttribute(included))
{
return false;
}

return !options.TryGetValue("build_property.PolySharpExcludeGeneratedTypes", out var excluded)
|| !ListContainsAttribute(excluded);
}

private static bool ListContainsAttribute(string list)
{
foreach (var entry in list.Split([';', ','], StringSplitOptions.RemoveEmptyEntries))
{
if (string.Equals(entry.Trim(), AttributeMetadataName, StringComparison.Ordinal))
{
return true;
}
}

return false;
}

private static bool HasAccessibleAttribute(Compilation compilation)
{
// GetTypeByMetadataName returns null when several references declare the type (common with
// polyfill packages), so inspect every candidate. The compiler ignores [Embedded] types from other
// assemblies, which is how Polyfill declares them, even when InternalsVisibleTo makes them accessible.
INamedTypeSymbol? accessible = null;
var accessibleCount = 0;

foreach (var type in compilation.GetTypesByMetadataName(AttributeMetadataName))
{
if (compilation.IsSymbolAccessibleWithin(type, compilation.Assembly)
&& !IsEmbeddedFromReference(type, compilation))
{
accessible = type;
accessibleCount++;
}
}

if (accessibleCount <= 1)
{
return accessible != null;
}

// Several accessible definitions are not always ambiguous (a declaration in source wins, for example).
// Only the compiler's own lookup gives the right answer, so bind the name the generated code uses and
// declare our own attribute only when that fails (CS0433). Ours is then in source, so it takes precedence
// over the referenced ones.
var tree = compilation.SyntaxTrees.FirstOrDefault();

if (tree is null)
{
// Speculative binding needs a tree, and other generators may still emit a module initializer.
tree = CSharpSyntaxTree.ParseText(string.Empty);
compilation = compilation.AddSyntaxTrees(tree);
}

var typeInfo = compilation.GetSemanticModel(tree).GetSpeculativeTypeInfo(
0,
SyntaxFactory.ParseTypeName("global::" + AttributeMetadataName),
SpeculativeBindingOption.BindAsTypeOrNamespace);

return typeInfo.Type is { TypeKind: not TypeKind.Error };
}

private static bool IsEmbeddedFromReference(INamedTypeSymbol type, Compilation compilation)
{
if (SymbolEqualityComparer.Default.Equals(type.ContainingAssembly, compilation.Assembly))
{
return false;
}

foreach (var attribute in type.GetAttributes())
{
if (attribute.AttributeClass is { Name: "EmbeddedAttribute", ContainingNamespace: { Name: "CodeAnalysis", ContainingNamespace: { Name: "Microsoft", ContainingNamespace.IsGlobalNamespace: true } } })
{
return true;
}
}

return false;
}
}
Loading
Loading