diff --git a/src/libraries/Common/tests/SourceGenerators/GlobalOptionsOnlyProvider.cs b/src/libraries/Common/tests/SourceGenerators/GlobalOptionsOnlyProvider.cs index 09b8b86af77d02..c44a9bf04ec75a 100644 --- a/src/libraries/Common/tests/SourceGenerators/GlobalOptionsOnlyProvider.cs +++ b/src/libraries/Common/tests/SourceGenerators/GlobalOptionsOnlyProvider.cs @@ -1,15 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Diagnostics; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.Interop; -using Microsoft.Interop.UnitTests; -using SourceGenerators.Tests; namespace SourceGenerators.Tests { @@ -28,23 +23,30 @@ public GlobalOptionsOnlyProvider(AnalyzerConfigOptions globalOptions) public sealed override AnalyzerConfigOptions GetOptions(SyntaxTree tree) { - return EmptyOptions.Instance; + return DictionaryAnalyzerConfigOptions.Empty; } public sealed override AnalyzerConfigOptions GetOptions(AdditionalText textFile) { - return EmptyOptions.Instance; + return DictionaryAnalyzerConfigOptions.Empty; } + } - private sealed class EmptyOptions : AnalyzerConfigOptions - { - public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) - { - value = null; - return false; - } + /// + /// An implementation of backed by an . + /// + internal sealed class DictionaryAnalyzerConfigOptions : AnalyzerConfigOptions + { + public static readonly DictionaryAnalyzerConfigOptions Empty = new(ImmutableDictionary.Empty); + + private readonly ImmutableDictionary _options; - public static AnalyzerConfigOptions Instance = new EmptyOptions(); + public DictionaryAnalyzerConfigOptions(ImmutableDictionary options) + { + _options = options; } + + public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) + => _options.TryGetValue(key, out value); } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Suppressor.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Suppressor.cs index 6e9a1448fcbfb7..3806567ca6ac54 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Suppressor.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Suppressor.cs @@ -1,11 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; +using System.Threading; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { @@ -35,6 +40,9 @@ public sealed class Suppressor : DiagnosticSuppressor public override void ReportSuppressions(SuppressionAnalysisContext context) { + // Lazily built set of locations that were actually intercepted by the generator. + HashSet? interceptedLocationKeys = null; + foreach (Diagnostic diagnostic in context.ReportedDiagnostics) { string diagnosticId = diagnostic.Id; @@ -51,22 +59,146 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) // The trim analyzer changed from warning on the InvocationExpression to the MemberAccessExpression in https://github.com/dotnet/runtime/pull/110086 // In other words, the warning location went from from `{|Method1(arg1, arg2)|}` to `{|Method1|}(arg1, arg2)` // To account for this, we need to check if the location is an InvocationExpression or a child of an InvocationExpression. - bool shouldSuppressDiagnostic = - location.SourceTree is SyntaxTree sourceTree && - sourceTree.GetRoot().FindNode(location.SourceSpan) is SyntaxNode syntaxNode && - (syntaxNode as InvocationExpressionSyntax ?? syntaxNode.Parent as InvocationExpressionSyntax) is InvocationExpressionSyntax invocation && - BinderInvocation.IsCandidateSyntaxNode(invocation) && - context.GetSemanticModel(sourceTree) - .GetOperation(invocation, context.CancellationToken) is IInvocationOperation operation && - BinderInvocation.IsBindingOperation(operation); - - if (shouldSuppressDiagnostic) + // Use getInnermostNodeForTie to handle the case where the binding call is an + // argument to another method (e.g. Some.Method(config.Get())). In that case, + // ArgumentSyntax and the inner InvocationExpressionSyntax can share the same span. + if (location.SourceTree is not SyntaxTree sourceTree) + { + continue; + } + + SyntaxNode syntaxNode = sourceTree.GetRoot(context.CancellationToken).FindNode(location.SourceSpan, getInnermostNodeForTie: true); + if ((syntaxNode as InvocationExpressionSyntax ?? syntaxNode.Parent as InvocationExpressionSyntax) is not InvocationExpressionSyntax invocation) + { + continue; + } + + if (!BinderInvocation.IsCandidateSyntaxNode(invocation)) + { + continue; + } + + SemanticModel semanticModel = context.GetSemanticModel(sourceTree); + if (semanticModel.GetOperation(invocation, context.CancellationToken) is not IInvocationOperation operation || + !BinderInvocation.IsBindingOperation(operation)) + { + continue; + } + + // Only suppress if the generator actually intercepted this call site. + // The generator may skip interception for unsupported types (https://github.com/dotnet/runtime/issues/96643). + interceptedLocationKeys ??= CollectInterceptedLocationKeys(context.Compilation, context.CancellationToken); + + string? locationKey = GetInvocationLocationKey(invocation, semanticModel, context.CancellationToken); + if (locationKey is null || !interceptedLocationKeys.Contains(locationKey)) + { + continue; + } + + SuppressionDescriptor targetSuppression = diagnosticId == RUCDiagnostic.SuppressedDiagnosticId + ? RUCDiagnostic + : RDCDiagnostic; + context.ReportSuppression(Suppression.Create(targetSuppression, diagnostic)); + } + } + + /// + /// Scans the generated source trees for [InterceptsLocation] attributes and collects + /// all intercepted locations. For v0, locations are (filePath, line, column) tuples. + /// For v1, locations are the encoded data strings from the attribute. + /// + private static HashSet CollectInterceptedLocationKeys(Compilation compilation, CancellationToken cancellationToken) + { + var keys = new HashSet(); + + foreach (SyntaxTree tree in compilation.SyntaxTrees) + { + if (!tree.FilePath.EndsWith("BindingExtensions.g.cs", System.StringComparison.Ordinal)) + { + continue; + } + + SyntaxNode root = tree.GetRoot(cancellationToken); + foreach (AttributeSyntax attr in root.DescendantNodes().OfType()) + { + // Matching the name like this is somewhat brittle, but it's okay as long as we match what the generator emits. + if (attr.Name.ToString() != "InterceptsLocation") + { + continue; + } + + AttributeArgumentListSyntax? argList = attr.ArgumentList; + if (argList is null) + { + continue; + } + + SeparatedSyntaxList args = argList.Arguments; + + if (InterceptorVersion == 0) + { + // v0 format: [InterceptsLocation("filePath", line, column)] + if (args.Count == 3 && + args[0].Expression is LiteralExpressionSyntax filePathLiteral && + filePathLiteral.IsKind(SyntaxKind.StringLiteralExpression) && + args[1].Expression is LiteralExpressionSyntax lineLiteral && + lineLiteral.IsKind(SyntaxKind.NumericLiteralExpression) && + args[2].Expression is LiteralExpressionSyntax colLiteral && + colLiteral.IsKind(SyntaxKind.NumericLiteralExpression)) + { + keys.Add(MakeV0Key(filePathLiteral.Token.ValueText, (int)lineLiteral.Token.Value!, (int)colLiteral.Token.Value!)); + } + } + else + { + // v1 format: [InterceptsLocation(version, "data")] + if (args.Count == 2 && + args[1].Expression is LiteralExpressionSyntax dataLiteral && + dataLiteral.IsKind(SyntaxKind.StringLiteralExpression)) + { + keys.Add(MakeV1Key(dataLiteral.Token.ValueText)); + } + } + } + } + + return keys; + } + + private static string MakeV0Key(string filePath, int line, int column) => $"{filePath}|{line}|{column}"; + + private static string MakeV1Key(string data) => data; + + private static string? GetInvocationLocationKey(InvocationExpressionSyntax invocation, SemanticModel semanticModel, CancellationToken cancellationToken) + { + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + { + return null; + } + + if (InterceptorVersion == 0) + { + SyntaxTree syntaxTree = memberAccess.SyntaxTree; + TextSpan nameSpan = memberAccess.Name.Span; + FileLinePositionSpan lineSpan = syntaxTree.GetLineSpan(nameSpan, cancellationToken); + + string filePath; + SourceReferenceResolver? resolver = semanticModel.Compilation.Options.SourceReferenceResolver; + filePath = resolver?.NormalizePath(syntaxTree.FilePath, baseFilePath: null) ?? syntaxTree.FilePath; + + return MakeV0Key(filePath, lineSpan.StartLinePosition.Line + 1, lineSpan.StartLinePosition.Character + 1); + } + else + { + object? interceptableLocation = GetInterceptableLocationFunc?.Invoke(semanticModel, invocation, cancellationToken); + if (interceptableLocation is null) { - SuppressionDescriptor targetSuppression = diagnosticId == RUCDiagnostic.SuppressedDiagnosticId - ? RUCDiagnostic - : RDCDiagnostic; - context.ReportSuppression(Suppression.Create(targetSuppression, diagnostic)); + return null; } + + string data = (string)InterceptableLocationDataGetter!.Invoke(interceptableLocation, parameters: null)!; + + return MakeV1Key(data); } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/InterceptorInfo.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/InterceptorInfo.cs index 5b4f903db7c210..a1e20eb7a796a3 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/InterceptorInfo.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/InterceptorInfo.cs @@ -33,7 +33,7 @@ public sealed record InterceptorInfo Debug.Assert((MethodsToGen.ConfigBinder_Bind & interceptor) is 0); ImmutableEquatableArray? infoList; - if ((MethodsToGen.ConfigBinder_Any ^ MethodsToGen.ConfigBinder_Bind & interceptor) is not 0) + if (((MethodsToGen.ConfigBinder_Any & ~MethodsToGen.ConfigBinder_Bind) & interceptor) is not 0) { infoList = ConfigBinder; } @@ -92,7 +92,7 @@ public void RegisterInterceptor(MethodsToGen overload, IInvocationOperation oper { Debug.Assert((MethodsToGen.ConfigBinder_Bind & overload) is 0); - if ((MethodsToGen.ConfigBinder_Any ^ MethodsToGen.ConfigBinder_Bind & overload) is not 0) + if (((MethodsToGen.ConfigBinder_Any & ~MethodsToGen.ConfigBinder_Bind) & overload) is not 0) { RegisterInterceptor(ref _interceptors_configBinder); } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs index 1e245d516d2653..9dd14fd5dc2886 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs @@ -99,6 +99,7 @@ private static async Task VerifyThatSourceIsGenerated(string testSourceCode) Assert.NotNull(source); Assert.Empty(result.Diagnostics); Assert.True(source.Value.SourceText.Lines.Count > 10); + await VerifySuppressedCallsMatchInterceptedCalls(result); } private static bool s_initializedInterceptorVersion; @@ -174,6 +175,8 @@ private static async Task VerifyAgainstBaselineUsingF Assert.True(resultEqualsBaseline, errorMessage); + await VerifySuppressedCallsMatchInterceptedCalls(result); + return result; } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs index 3db19473fe2f48..d74ac253e616eb 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs @@ -9,13 +9,18 @@ using System.Linq; using System.Text; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading.Tasks; +using ILLink.RoslynAnalyzer; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Binder.SourceGeneration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using SourceGenerators.Tests; using Xunit; namespace Microsoft.Extensions.SourceGeneration.Configuration.Binder.Tests @@ -535,5 +540,245 @@ public static void Main() Diagnostic diagnostic = Assert.Single(effective, d => d.Id == "SYSLIB1103"); Assert.False(diagnostic.IsSuppressed); } + + /// + /// Verifies that the suppressor suppresses IL2026/IL3050 when a ConfigurationBinder call + /// is passed directly as a method argument (e.g. Some.Method(config.Get<T>())). + /// Regression test for https://github.com/dotnet/runtime/issues/94544. + /// + [Fact] + public async Task Suppressor_SuppressesWarnings_WhenBindingCallIsMethodArgument() + { + string source = """ + using Microsoft.Extensions.Configuration; + + public class Program + { + public static void Main() + { + IConfigurationSection c = new ConfigurationBuilder().Build().GetSection("Options"); + Some.Method(c.Get()); + } + } + + internal static class Some + { + public static void Method(MyOptions? options) { } + } + + public class MyOptions + { + public int MaxRetries { get; set; } + } + """; + + ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source); + Assert.NotNull(result.GeneratedSource); + + await VerifySuppressedCallsMatchInterceptedCalls(result); + } + + /// + /// Verifies that the suppressor also works for the straightforward assignment case, + /// ensuring no regression in existing behavior. + /// + [Fact] + public async Task Suppressor_SuppressesWarnings_ForSimpleBindingCall() + { + string source = """ + using Microsoft.Extensions.Configuration; + + public class Program + { + public static void Main() + { + IConfigurationSection c = new ConfigurationBuilder().Build().GetSection("Options"); + var options = c.Get(); + } + } + + public class MyOptions + { + public int MaxRetries { get; set; } + } + """; + + ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source); + Assert.NotNull(result.GeneratedSource); + + await VerifySuppressedCallsMatchInterceptedCalls(result); + } + + [Fact] + public async Task Suppressor_SuppressesWarnings_WithLineDirective() + { + string source = """ + using Microsoft.Extensions.Configuration; + + public class Program + { + public static void Main() + { + IConfigurationSection c = new ConfigurationBuilder().Build().GetSection("Options"); + #line 100 "Remapped.cs" + var options = c.Get(); + #line default + } + } + + public class MyOptions + { + public int MaxRetries { get; set; } + } + """; + + ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source); + Assert.NotNull(result.GeneratedSource); + + await VerifySuppressedCallsMatchInterceptedCalls(result); + } + + /// + /// Verifies that the set of IL2026/IL3050 diagnostics suppressed by the suppressor + /// matches exactly the set of calls intercepted by the source generator. + /// Catches both under-suppression (https://github.com/dotnet/runtime/issues/94544) + /// and over-suppression (https://github.com/dotnet/runtime/issues/96643). + /// + private static async Task VerifySuppressedCallsMatchInterceptedCalls(ConfigBindingGenRunResult result) + { + Assert.NotNull(result.GenerationSpec); + + // Collect all intercepted (line, column) locations from the generator spec. + // The interceptor targets MemberAccessExpression.Name (e.g. "Get" in "c.Get()"). + HashSet<(int Line, int Column)> interceptedLocations = GetInterceptedLocations(result.GenerationSpec); + Assert.NotEmpty(interceptedLocations); + + // Run the ILLink analyzer + suppressor on the output compilation (which includes generated InterceptsLocation attributes). + ImmutableArray diagnostics = await GetDiagnosticsWithSuppressor(result.OutputCompilation); + + // The ILLink analyzer must have produced at least one IL2026 or IL3050 that was suppressed. + // Without this, the assertions below would pass vacuously if the analyzer didn't fire. + Assert.Contains(diagnostics, d => (d.Id is "IL2026" or "IL3050") && d.IsSuppressed); + + // Every suppressed IL2026/IL3050 diagnostic should be at an intercepted location. + foreach (Diagnostic d in diagnostics.Where(d => (d.Id is "IL2026" or "IL3050") && d.IsSuppressed)) + { + (int line, int column) = GetMethodNameLocation(d); + Assert.True(interceptedLocations.Contains((line, column)), + $"Suppressed {d.Id} at ({line},{column}) but no interceptor was generated for that call site."); + } + + // Every intercepted location should have its IL2026/IL3050 diagnostics suppressed. + foreach (Diagnostic d in diagnostics.Where(d => (d.Id is "IL2026" or "IL3050") && !d.IsSuppressed)) + { + (int line, int column) = GetMethodNameLocation(d); + Assert.False(interceptedLocations.Contains((line, column)), + $"Unsuppressed {d.Id} at ({line},{column}) but an interceptor was generated for that call site."); + } + } + + /// + /// Resolves a diagnostic's location to the method name position that the interceptor targets. + /// The ILLink analyzer reports on the MemberAccessExpression (e.g. "c.Get<T>"), + /// but the interceptor targets just the Name part (e.g. "Get"). This method walks from + /// the diagnostic location to the InvocationExpression's MemberAccessExpression.Name + /// to get the matching (line, column). + /// + private static (int Line, int Column) GetMethodNameLocation(Diagnostic diagnostic) + { + Location location = diagnostic.AdditionalLocations.Count > 0 + ? diagnostic.AdditionalLocations[0] + : diagnostic.Location; + SyntaxTree sourceTree = location.SourceTree!; + SyntaxNode node = sourceTree.GetRoot().FindNode(location.SourceSpan, getInnermostNodeForTie: true); + + InvocationExpressionSyntax invocation = (node as InvocationExpressionSyntax + ?? node.Parent as InvocationExpressionSyntax)!; + + var memberAccess = (MemberAccessExpressionSyntax)invocation.Expression; + FileLinePositionSpan nameSpan = sourceTree.GetLineSpan(memberAccess.Name.Span); + + return (nameSpan.StartLinePosition.Line + 1, nameSpan.StartLinePosition.Character + 1); + } + + private static HashSet<(int Line, int Column)> GetInterceptedLocations(SourceGenerationSpec spec) + { + var locations = new HashSet<(int, int)>(); + InterceptorInfo info = spec.InterceptorInfo; + + AddLocations(info.ConfigBinder); + AddLocations(info.OptionsBuilderExt); + AddLocations(info.ServiceCollectionExt); + AddTypedLocations(info.ConfigBinder_Bind_instance); + AddTypedLocations(info.ConfigBinder_Bind_instance_BinderOptions); + AddTypedLocations(info.ConfigBinder_Bind_key_instance); + + return locations; + + void AddLocations(IEnumerable? locationInfos) + { + if (locationInfos is null) + return; + + foreach (InvocationLocationInfo loc in locationInfos) + { + locations.Add(GetLocation(loc)); + } + } + + void AddTypedLocations(IEnumerable? typedInfos) + { + if (typedInfos is null) + return; + + foreach (TypedInterceptorInvocationInfo typed in typedInfos) + { + AddLocations(typed.Locations); + } + } + } + + private static (int Line, int Column) GetLocation(InvocationLocationInfo loc) + { + if (loc.LineNumber != 0) + { + return (loc.LineNumber, loc.CharacterNumber); + } + + // v1 interceptor: parse from display location, e.g. "path(line,col)" + string display = loc.InterceptableLocationGetDisplayLocation(); + Match match = Regex.Match(display, @"\((\d+),(\d+)\)$"); + Assert.True(match.Success, $"Could not parse display location: {display}"); + + return (int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture), + int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture)); + } + + private static async Task> GetDiagnosticsWithSuppressor(Compilation compilation) + { + var analyzers = ImmutableArray.Create( + new DynamicallyAccessedMembersAnalyzer(), + new ConfigurationBindingGenerator.Suppressor()); + + var trimAotAnalyzerOptions = new DictionaryAnalyzerConfigOptions( + ImmutableDictionary.CreateRange( + StringComparer.OrdinalIgnoreCase, + [ + new("build_property.EnableTrimAnalyzer", "true"), + new("build_property.EnableAotAnalyzer", "true"), + ])); + var analyzerOptions = new AnalyzerOptions( + ImmutableArray.Empty, + new GlobalOptionsOnlyProvider(trimAotAnalyzerOptions)); + var options = new CompilationWithAnalyzersOptions( + analyzerOptions, + onAnalyzerException: null, + concurrentAnalysis: true, + logAnalyzerExecutionTime: false, + reportSuppressedDiagnostics: true); + + return await new CompilationWithAnalyzers(compilation, analyzers, options) + .GetAllDiagnosticsAsync(); + } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj index ddc4d17d10f019..299c8afc31652e 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);$(NetFrameworkCurrent) @@ -25,6 +25,7 @@ + @@ -46,6 +47,7 @@ + diff --git a/src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/ComInterfaceGenerator.Unit.Tests.csproj b/src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/ComInterfaceGenerator.Unit.Tests.csproj index c612c394035dbd..9ce73c44b35c9e 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/ComInterfaceGenerator.Unit.Tests.csproj +++ b/src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/ComInterfaceGenerator.Unit.Tests.csproj @@ -11,7 +11,6 @@ - diff --git a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/LibraryImportGenerator.Unit.Tests.csproj b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/LibraryImportGenerator.Unit.Tests.csproj index 253e99c7fb0832..c86b834a9a2957 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/LibraryImportGenerator.Unit.Tests.csproj +++ b/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/LibraryImportGenerator.Unit.Tests.csproj @@ -15,7 +15,6 @@ - diff --git a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/ILLink.RoslynAnalyzer.Tests.csproj b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/ILLink.RoslynAnalyzer.Tests.csproj index 184616fc012c10..3f94c61f198d2f 100644 --- a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/ILLink.RoslynAnalyzer.Tests.csproj +++ b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/ILLink.RoslynAnalyzer.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/TestCaseCompilation.cs b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/TestCaseCompilation.cs index 5ec67fd1f9742e..6e1d5b070749f9 100644 --- a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/TestCaseCompilation.cs +++ b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/TestCaseCompilation.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; +using SourceGenerators.Tests; namespace ILLink.RoslynAnalyzer.Tests { @@ -87,7 +88,7 @@ public static (CompilationWithAnalyzers Compilation, SemanticModel SemanticModel })); var analyzerOptions = new AnalyzerOptions( additionalFiles: additionalFiles?.ToImmutableArray() ?? ImmutableArray.Empty, - new SimpleAnalyzerOptions(globalAnalyzerOptions)); + new GlobalOptionsOnlyProvider(CreateGlobalOptions(globalAnalyzerOptions))); var exceptionDiagnostics = new List(); @@ -103,40 +104,16 @@ public static (CompilationWithAnalyzers Compilation, SemanticModel SemanticModel return (new CompilationWithAnalyzers(comp, SupportedDiagnosticAnalyzers, compWithAnalyzerOptions), comp.GetSemanticModel(src), exceptionDiagnostics); } - sealed class SimpleAnalyzerOptions : AnalyzerConfigOptionsProvider + private static DictionaryAnalyzerConfigOptions CreateGlobalOptions((string, string)[]? globalOptions) { - public SimpleAnalyzerOptions((string, string)[]? globalOptions) + if (globalOptions is null or { Length: 0 }) { - globalOptions ??= Array.Empty<(string, string)>(); - GlobalOptions = new SimpleAnalyzerConfigOptions(ImmutableDictionary.CreateRange( - StringComparer.OrdinalIgnoreCase, - globalOptions.Select(x => new KeyValuePair(x.Item1, x.Item2)))); + return DictionaryAnalyzerConfigOptions.Empty; } - public override AnalyzerConfigOptions GlobalOptions { get; } - - public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) - => SimpleAnalyzerConfigOptions.Empty; - - public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) - => SimpleAnalyzerConfigOptions.Empty; - - sealed class SimpleAnalyzerConfigOptions : AnalyzerConfigOptions - { - public static readonly SimpleAnalyzerConfigOptions Empty = new SimpleAnalyzerConfigOptions(ImmutableDictionary.Empty); - - private readonly ImmutableDictionary _dict; - public SimpleAnalyzerConfigOptions(ImmutableDictionary dict) - { - _dict = dict; - } - - // Suppress warning about missing nullable attributes -#pragma warning disable 8765 - public override bool TryGetValue(string key, out string? value) - => _dict.TryGetValue(key, out value); -#pragma warning restore 8765 - } + return new DictionaryAnalyzerConfigOptions(ImmutableDictionary.CreateRange( + StringComparer.OrdinalIgnoreCase, + globalOptions.Select(x => new KeyValuePair(x.Item1, x.Item2)))); } } }