diff --git a/TUnit.Analyzers.Tests/MissingPolyfillAnalyzerTests.cs b/TUnit.Analyzers.Tests/MissingPolyfillAnalyzerTests.cs index 3ec986a8e5..0cd8e13854 100644 --- a/TUnit.Analyzers.Tests/MissingPolyfillAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/MissingPolyfillAnalyzerTests.cs @@ -1,3 +1,5 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Testing; using Verifier = TUnit.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier; @@ -48,4 +50,78 @@ public class MyClass .WithArguments("System.Runtime.CompilerServices.ModuleInitializerAttribute") ); } + + [Test] + public async Task No_Error_When_ModuleInitializerAttribute_Provided_By_Polyfill() + { + var net48References = new ReferenceAssemblies( + "net48", + new PackageIdentity("Microsoft.NETFramework.ReferenceAssemblies.net48", "1.0.3"), + System.IO.Path.Combine("ref", "net48")); + + await Verifier + .VerifyAnalyzerAsync( + """ + namespace System.Runtime.CompilerServices + { + internal sealed class ModuleInitializerAttribute : Attribute + { + } + } + + public class MyClass + { + } + """, + test => + { + test.ReferenceAssemblies = net48References; + test.TestState.AdditionalReferences.Clear(); + test.CompilerDiagnostics = CompilerDiagnostics.None; + } + ); + } + + [Test] + public async Task No_Error_When_ModuleInitializerAttribute_In_Multiple_Referenced_Assemblies() + { + // On modern TFMs, ModuleInitializerAttribute is in the runtime. + // When a referenced library also embeds it via Polyfill, + // GetTypeByMetadataName returns null due to ambiguity. + // The analyzer should not report TUnit0073 in this case. + var refs = await ReferenceAssemblies.Net.Net90.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); + + var attributeSource = CSharpSyntaxTree.ParseText(""" + namespace System.Runtime.CompilerServices + { + internal sealed class ModuleInitializerAttribute : Attribute + { + } + } + """); + + using var stream = new MemoryStream(); + var result = CSharpCompilation.Create("PolyfillLibrary", + [attributeSource], + refs, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .Emit(stream); + await TUnit.Assertions.Assert.That(result.Success).IsTrue(); + + await Verifier + .VerifyAnalyzerAsync( + """ + public class MyClass + { + } + """, + test => + { + test.TestState.AdditionalReferences.Clear(); + test.TestState.AdditionalReferences.Add( + MetadataReference.CreateFromImage(stream.ToArray())); + test.CompilerDiagnostics = CompilerDiagnostics.None; + } + ); + } } diff --git a/TUnit.Analyzers/MissingPolyfillAnalyzer.cs b/TUnit.Analyzers/MissingPolyfillAnalyzer.cs index 3d84eb10c4..4bd69e4ffc 100644 --- a/TUnit.Analyzers/MissingPolyfillAnalyzer.cs +++ b/TUnit.Analyzers/MissingPolyfillAnalyzer.cs @@ -22,11 +22,34 @@ private static void AnalyzeCompilation(CompilationAnalysisContext context) { foreach (var typeName in RequiredTypes) { - if (context.Compilation.GetTypeByMetadataName(typeName) is null) + if (!HasType(context.Compilation, typeName)) { context.ReportDiagnostic( Diagnostic.Create(Rules.MissingPolyfillPackage, Location.None, typeName)); } } } + + private static bool HasType(Compilation compilation, string typeName) + { + // GetTypeByMetadataName returns null when the type exists in multiple + // referenced assemblies (e.g. when multiple libraries embed + // ModuleInitializerAttribute via Polyfill). Check each assembly + // individually to avoid this ambiguity problem. + if (compilation.GetTypeByMetadataName(typeName) is not null) + { + return true; + } + + foreach (var reference in compilation.References) + { + if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly && + assembly.GetTypeByMetadataName(typeName) is not null) + { + return true; + } + } + + return false; + } }