diff --git a/src/Compilers/CSharp/Test/Emit3/Diagnostics/DiagnosticAnalyzerTests.cs b/src/Compilers/CSharp/Test/Emit3/Diagnostics/DiagnosticAnalyzerTests.cs index ad1e6be964805..bb6dc5aecabcb 100644 --- a/src/Compilers/CSharp/Test/Emit3/Diagnostics/DiagnosticAnalyzerTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/Diagnostics/DiagnosticAnalyzerTests.cs @@ -23,6 +23,7 @@ using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; +using Roslyn.Test.Utilities.TestGenerators; using Roslyn.Utilities; using Xunit; @@ -4441,5 +4442,203 @@ record B(int I) : A(I); var diagnostics = await compWithAnalyzers.GetAnalyzerSemanticDiagnosticsAsync(model, filterSpan: null, CancellationToken.None); diagnostics.Verify(Diagnostic("ID0001", "B").WithLocation(1, 8)); } + + private sealed class OptionsOverrideDiagnosticAnalyzer(AnalyzerConfigOptionsProvider customOptions) : DiagnosticAnalyzer + { + private static readonly DiagnosticDescriptor s_descriptor = new DiagnosticDescriptor( + id: "ID0001", + title: "Title", + messageFormat: "Message", + category: "Category", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + private readonly AnalyzerConfigOptionsProvider _customOptions = customOptions; + + public override ImmutableArray SupportedDiagnostics => [s_descriptor]; + + public bool RegisterAdditionalFileActionInvoked { get; private set; } + public bool RegisterCodeBlockActionInvoked { get; private set; } + public bool RegisterCodeBlockStartActionInvoked { get; private set; } + public bool RegisterCompilationActionInvoked { get; private set; } + public bool RegisterOperationActionInvoked { get; private set; } + public bool RegisterOperationBlockActionInvoked { get; private set; } + public bool RegisterSemanticModelActionInvoked { get; private set; } + public bool RegisterSymbolActionInvoked { get; private set; } + public bool RegisterSyntaxNodeActionInvoked { get; private set; } + public bool RegisterSyntaxTreeActionInvoked { get; private set; } + + public bool RegisterOperationBlockStartActionInvoked { get; private set; } + public bool RegisterOperationBlockEndActionInvoked { get; private set; } + public bool RegisterCompilationStartActionInvoked { get; private set; } + public bool RegisterCompilationEndActionInvoked { get; private set; } + public bool RegisterSymbolStartActionInvoked { get; private set; } + public bool RegisterSymbolEndActionInvoked { get; private set; } + + public AnalyzerOptions SeenOptions; + + private void AssertSame(AnalyzerOptions options) + { + // First, assert that the options provider we see is the custom one the test sets. + Assert.Same(options.AnalyzerConfigOptionsProvider, _customOptions); + + if (SeenOptions is null) + SeenOptions = options; + + // Also ensure that the compiler actually passes the same AnalyzerOptions wrapper around + // the options provider. That ensures we're not accidentally creating new instances unnecessarily. + Assert.Same(SeenOptions, options); + } + + public override void Initialize(AnalysisContext context) + { + context.RegisterAdditionalFileAction(context => { AssertSame(context.Options); RegisterAdditionalFileActionInvoked = true; }); + context.RegisterCodeBlockAction(context => { AssertSame(context.Options); RegisterCodeBlockActionInvoked = true; }); + context.RegisterCodeBlockStartAction(context => { AssertSame(context.Options); RegisterCodeBlockStartActionInvoked = true; }); + context.RegisterCompilationAction(context => { AssertSame(context.Options); RegisterCompilationActionInvoked = true; }); + context.RegisterOperationAction(context => { AssertSame(context.Options); RegisterOperationActionInvoked = true; }, OperationKind.Block); + context.RegisterOperationBlockAction(context => { AssertSame(context.Options); RegisterOperationBlockActionInvoked = true; }); + context.RegisterSemanticModelAction(context => { AssertSame(context.Options); RegisterSemanticModelActionInvoked = true; }); + context.RegisterSymbolAction(context => { AssertSame(context.Options); RegisterSymbolActionInvoked = true; }, SymbolKind.NamedType); + context.RegisterSyntaxNodeAction(context => { AssertSame(context.Options); RegisterSyntaxNodeActionInvoked = true; }, SyntaxKind.ClassDeclaration); + context.RegisterSyntaxTreeAction(context => { AssertSame(context.Options); RegisterSyntaxTreeActionInvoked = true; }); + + context.RegisterOperationBlockStartAction(context => + { + AssertSame(context.Options); + RegisterOperationBlockStartActionInvoked = true; + context.RegisterOperationBlockEndAction(context => + { + AssertSame(context.Options); + RegisterOperationBlockEndActionInvoked = true; + }); + }); + + context.RegisterCompilationStartAction(context => + { + AssertSame(context.Options); + RegisterCompilationStartActionInvoked = true; + context.RegisterCompilationEndAction(context => + { + AssertSame(context.Options); + RegisterCompilationEndActionInvoked = true; + }); + }); + context.RegisterSymbolStartAction(context => + { + AssertSame(context.Options); + RegisterSymbolStartActionInvoked = true; + context.RegisterSymbolEndAction(context => + { + AssertSame(context.Options); + RegisterSymbolEndActionInvoked = true; + }); + }, SymbolKind.NamedType); + } + + public void AssertAllCallbacksInvoked() + { + Assert.NotNull(SeenOptions); + + Assert.True(RegisterAdditionalFileActionInvoked); + + Assert.True(RegisterAdditionalFileActionInvoked); + Assert.True(RegisterCodeBlockActionInvoked); + Assert.True(RegisterCodeBlockStartActionInvoked); + Assert.True(RegisterCompilationActionInvoked); + Assert.True(RegisterOperationActionInvoked); + Assert.True(RegisterOperationBlockActionInvoked); + Assert.True(RegisterSemanticModelActionInvoked); + Assert.True(RegisterSymbolActionInvoked); + Assert.True(RegisterSyntaxNodeActionInvoked); + Assert.True(RegisterSyntaxTreeActionInvoked); + + Assert.True(RegisterOperationBlockStartActionInvoked); + Assert.True(RegisterOperationBlockEndActionInvoked); + Assert.True(RegisterCompilationStartActionInvoked); + Assert.True(RegisterCompilationEndActionInvoked); + Assert.True(RegisterSymbolStartActionInvoked); + Assert.True(RegisterSymbolEndActionInvoked); + } + } + + [Fact] + public async Task TestAnalyzerSpecificOptionsFactory() + { + // lang=C#-Test + string source = """ + class C + { + void M() + { + int x = 0; + } + } + """; + + var tree = CSharpSyntaxTree.ParseText(source); + var compilation = CreateCompilationWithCSharp(new[] { tree, CSharpSyntaxTree.ParseText(IsExternalInitTypeDefinition) }); + compilation.VerifyDiagnostics( + // (5,13): warning CS0219: The variable 'x' is assigned but its value is never used + // int x = 0; + Diagnostic(ErrorCode.WRN_UnreferencedVarAssg, "x").WithArguments("x").WithLocation(5, 13)); + + var additionalText = new InMemoryAdditionalText("path", "content"); + + // Ensure that the analyzer only sees the custom options passed to the callbacks, and never the shared options. + var sharedOptions = new AnalyzerOptions([additionalText]); + + // Test1. Just a single analyzer. Ensure all callbacks get the custom options. + { + var customOptions = new CompilerAnalyzerConfigOptionsProvider( + ImmutableDictionary.Empty, + new DictionaryAnalyzerConfigOptions( + ImmutableDictionary.Empty)); + Assert.NotSame(sharedOptions, customOptions); + + var analyzer = new OptionsOverrideDiagnosticAnalyzer(customOptions); + + var compWithAnalyzers = new CompilationWithAnalyzers( + compilation, + [analyzer], + new CompilationWithAnalyzersOptions( + sharedOptions, onAnalyzerException: null, concurrentAnalysis: false, logAnalyzerExecutionTime: false, reportSuppressedDiagnostics: false, analyzerExceptionFilter: null, + _ => customOptions)); + + var diagnostics = await compWithAnalyzers.GetAllDiagnosticsAsync(); + Assert.Single(diagnostics); + + analyzer.AssertAllCallbacksInvoked(); + } + + // Test2. Two analyzers. Ensure both gets the custom options across all callbacks. + // Also, ensure that across the analyzers we're getting the exact same AnalyzerOptions instance. + { + var customOptions = new CompilerAnalyzerConfigOptionsProvider( + ImmutableDictionary.Empty, + new DictionaryAnalyzerConfigOptions( + ImmutableDictionary.Empty)); + Assert.NotSame(sharedOptions, customOptions); + + var analyzer1 = new OptionsOverrideDiagnosticAnalyzer(customOptions); + var analyzer2 = new OptionsOverrideDiagnosticAnalyzer(customOptions); + + var compWithAnalyzers = new CompilationWithAnalyzers( + compilation, + [analyzer1, analyzer2], + new CompilationWithAnalyzersOptions( + sharedOptions, onAnalyzerException: null, concurrentAnalysis: false, logAnalyzerExecutionTime: false, reportSuppressedDiagnostics: false, analyzerExceptionFilter: null, + _ => customOptions)); + + var diagnostics = await compWithAnalyzers.GetAllDiagnosticsAsync(); + Assert.Single(diagnostics); + + analyzer1.AssertAllCallbacksInvoked(); + analyzer2.AssertAllCallbacksInvoked(); + + // Both analyzers should get the exact same AnalyzerOptions instance since they used the same customOptions. + Assert.Same(analyzer1.SeenOptions, analyzer2.SeenOptions); + } + } } } diff --git a/src/Compilers/Core/CodeAnalysisTest/Diagnostics/DiagnosticLocalizationTests.cs b/src/Compilers/Core/CodeAnalysisTest/Diagnostics/DiagnosticLocalizationTests.cs index 9848453306a68..6c9685563c072 100644 --- a/src/Compilers/Core/CodeAnalysisTest/Diagnostics/DiagnosticLocalizationTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/Diagnostics/DiagnosticLocalizationTests.cs @@ -304,11 +304,21 @@ private static void TestDescriptorIsExceptionSafeCore(DiagnosticDescriptor descr Action onAnalyzerException = (ex, a, diag, ct) => exceptionDiagnostics.Add(diag); var analyzerManager = new AnalyzerManager(analyzer); var compilation = CSharp.CSharpCompilation.Create("test"); - var analyzerExecutor = AnalyzerExecutor.Create(compilation, AnalyzerOptions.Empty, - addNonCategorizedDiagnostic: (_, _) => { }, onAnalyzerException, analyzerExceptionFilter: null, - isCompilerAnalyzer: _ => false, analyzerManager, shouldSkipAnalysisOnGeneratedCode: _ => false, - shouldSuppressGeneratedCodeDiagnostic: (_, _, _, _) => false, isGeneratedCodeLocation: (_, _, _) => false, - isAnalyzerSuppressedForTree: (_, _, _, _) => false, getAnalyzerGate: _ => null, + var analyzerExecutor = AnalyzerExecutor.Create( + compilation, + AnalyzerOptions.Empty, + addNonCategorizedDiagnostic: (_, _, _) => { }, + onAnalyzerException, + analyzerExceptionFilter: null, + isCompilerAnalyzer: _ => false, + diagnosticAnalyzers: [analyzer], + getAnalyzerConfigOptionsProvider: null, + analyzerManager, + shouldSkipAnalysisOnGeneratedCode: _ => false, + shouldSuppressGeneratedCodeDiagnostic: (_, _, _, _) => false, + isGeneratedCodeLocation: (_, _, _) => false, + isAnalyzerSuppressedForTree: (_, _, _, _) => false, + getAnalyzerGate: _ => null, getSemanticModel: tree => compilation.GetSemanticModel(tree, ignoreAccessibility: true), SeverityFilter.None); var descriptors = analyzerManager.GetSupportedDiagnosticDescriptors(analyzer, analyzerExecutor, CancellationToken.None); diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisScope.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisScope.cs index d7f251955cd6a..938087e5f90f0 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisScope.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisScope.cs @@ -95,20 +95,19 @@ internal class AnalysisScope public static AnalysisScope Create(Compilation compilation, ImmutableArray analyzers, CompilationWithAnalyzers compilationWithAnalyzers) { - var analyzerOptions = compilationWithAnalyzers.AnalysisOptions.Options; + var additionalFiles = compilationWithAnalyzers.AnalysisOptions.Options.GetAdditionalFiles(); var hasAllAnalyzers = ComputeHasAllAnalyzers(analyzers, compilationWithAnalyzers); var concurrentAnalysis = compilationWithAnalyzers.AnalysisOptions.ConcurrentAnalysis; - return Create(compilation, analyzerOptions, analyzers, hasAllAnalyzers, concurrentAnalysis); + return Create(compilation, additionalFiles, analyzers, hasAllAnalyzers, concurrentAnalysis); } - public static AnalysisScope CreateForBatchCompile(Compilation compilation, AnalyzerOptions analyzerOptions, ImmutableArray analyzers) + public static AnalysisScope CreateForBatchCompile(Compilation compilation, ImmutableArray additionalFiles, ImmutableArray analyzers) { - return Create(compilation, analyzerOptions, analyzers, hasAllAnalyzers: true, concurrentAnalysis: compilation.Options.ConcurrentBuild); + return Create(compilation, additionalFiles, analyzers, hasAllAnalyzers: true, concurrentAnalysis: compilation.Options.ConcurrentBuild); } - private static AnalysisScope Create(Compilation compilation, AnalyzerOptions? analyzerOptions, ImmutableArray analyzers, bool hasAllAnalyzers, bool concurrentAnalysis) + private static AnalysisScope Create(Compilation compilation, ImmutableArray additionalFiles, ImmutableArray analyzers, bool hasAllAnalyzers, bool concurrentAnalysis) { - var additionalFiles = analyzerOptions?.AdditionalFiles ?? ImmutableArray.Empty; return new AnalysisScope(compilation.CommonSyntaxTrees, additionalFiles, analyzers, hasAllAnalyzers, filterFile: null, filterSpanOpt: null, originalFilterFile: null, originalFilterSpan: null, isSyntacticSingleFileAnalysis: false, diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs index 4efe2326b1753..385e075a73c33 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs @@ -476,23 +476,29 @@ internal void Initialize( var diagnosticQueue = DiagnosticQueue.Create(categorizeDiagnostics); var suppressedDiagnosticIds = trackSuppressedDiagnosticIds ? new ConcurrentSet() : null; - Action? addNotCategorizedDiagnostic = null; - Action? addCategorizedLocalDiagnostic = null; - Action? addCategorizedNonLocalDiagnostic = null; + Action? addNotCategorizedDiagnostic = null; + Action? addCategorizedLocalDiagnostic = null; + Action? addCategorizedNonLocalDiagnostic = null; if (categorizeDiagnostics) { - addCategorizedLocalDiagnostic = GetDiagnosticSink(diagnosticQueue.EnqueueLocal, compilation, analysisOptions.Options, _severityFilter, suppressedDiagnosticIds); - addCategorizedNonLocalDiagnostic = GetDiagnosticSink(diagnosticQueue.EnqueueNonLocal, compilation, analysisOptions.Options, _severityFilter, suppressedDiagnosticIds); + addCategorizedLocalDiagnostic = GetDiagnosticSink(diagnosticQueue.EnqueueLocal, compilation, _severityFilter, suppressedDiagnosticIds); + addCategorizedNonLocalDiagnostic = GetDiagnosticSink(diagnosticQueue.EnqueueNonLocal, compilation, _severityFilter, suppressedDiagnosticIds); } else { - addNotCategorizedDiagnostic = GetDiagnosticSink(diagnosticQueue.Enqueue, compilation, analysisOptions.Options, _severityFilter, suppressedDiagnosticIds); + addNotCategorizedDiagnostic = GetDiagnosticSink(diagnosticQueue.Enqueue, compilation, _severityFilter, suppressedDiagnosticIds); } // Wrap onAnalyzerException to pass in filtered diagnostic. - Action newOnAnalyzerException = (ex, analyzer, diagnostic, cancellationToken) => + var options = analysisOptions.Options ?? AnalyzerOptions.Empty; + + var newOnAnalyzerException = (Exception ex, DiagnosticAnalyzer analyzer, Diagnostic diagnostic, CancellationToken cancellationToken) => { - var filteredDiagnostic = GetFilteredDiagnostic(diagnostic, compilation, analysisOptions.Options, _severityFilter, suppressedDiagnosticIds, cancellationToken); + // Note: in this callback, it's fine/correct to use analysisOptions.Options instead of any diagnostic analyzer + // specific options. That's because the options are only used for filtering/determining-severities. But we + // do not allow analyzers to control the filtering/severity around the reporting of analyzer exceptions themselves. + // These are always passed through and reported to the user. + var filteredDiagnostic = GetFilteredDiagnostic(diagnostic, compilation, options, _severityFilter, suppressedDiagnosticIds, cancellationToken); if (filteredDiagnostic != null) { if (analysisOptions.OnAnalyzerException != null) @@ -501,18 +507,19 @@ internal void Initialize( } else if (categorizeDiagnostics) { - addCategorizedNonLocalDiagnostic!(filteredDiagnostic, analyzer, cancellationToken); + addCategorizedNonLocalDiagnostic!(filteredDiagnostic, analyzer, options, cancellationToken); } else { - addNotCategorizedDiagnostic!(filteredDiagnostic, cancellationToken); + addNotCategorizedDiagnostic!(filteredDiagnostic, options, cancellationToken); } } }; var analyzerExecutor = AnalyzerExecutor.Create( - compilation, analysisOptions.Options ?? AnalyzerOptions.Empty, addNotCategorizedDiagnostic, newOnAnalyzerException, analysisOptions.AnalyzerExceptionFilter, - IsCompilerAnalyzer, AnalyzerManager, ShouldSkipAnalysisOnGeneratedCode, ShouldSuppressGeneratedCodeDiagnostic, IsGeneratedOrHiddenCodeLocation, IsAnalyzerSuppressedForTree, GetAnalyzerGate, + compilation, options, addNotCategorizedDiagnostic, newOnAnalyzerException, analysisOptions.AnalyzerExceptionFilter, + IsCompilerAnalyzer, analysisScope.Analyzers, analysisOptions.GetAnalyzerConfigOptionsProvider, + AnalyzerManager, ShouldSkipAnalysisOnGeneratedCode, ShouldSuppressGeneratedCodeDiagnostic, IsGeneratedOrHiddenCodeLocation, IsAnalyzerSuppressedForTree, GetAnalyzerGate, getSemanticModel: GetOrCreateSemanticModel, _severityFilter, analysisOptions.LogAnalyzerExecutionTime, addCategorizedLocalDiagnostic, addCategorizedNonLocalDiagnostic, s => _programmaticSuppressions!.Add(s)); @@ -852,7 +859,7 @@ internal static AnalyzerDriver CreateAndAttachToCompilation( var categorizeDiagnostics = false; var analysisOptions = new CompilationWithAnalyzersOptions(options, onAnalyzerException, analyzerExceptionFilter: analyzerExceptionFilter, concurrentAnalysis: true, logAnalyzerExecutionTime: reportAnalyzer, reportSuppressedDiagnostics: false); - var analysisScope = AnalysisScope.CreateForBatchCompile(newCompilation, options, analyzers); + var analysisScope = AnalysisScope.CreateForBatchCompile(newCompilation, options.GetAdditionalFiles(), analyzers); analyzerDriver.Initialize(newCompilation, analysisOptions, new CompilationData(newCompilation), analysisScope, categorizeDiagnostics, trackSuppressedDiagnosticIds, cancellationToken); analyzerDriver.AttachQueueAndStartProcessingEvents(newCompilation.EventQueue!, analysisScope, usingPrePopulatedEventQueue: false, cancellationToken); @@ -1962,9 +1969,9 @@ private void ExecuteCompilationActions( } } - internal static Action GetDiagnosticSink(Action addDiagnosticCore, Compilation compilation, AnalyzerOptions? analyzerOptions, SeverityFilter severityFilter, ConcurrentSet? suppressedDiagnosticIds) + internal static Action GetDiagnosticSink(Action addDiagnosticCore, Compilation compilation, SeverityFilter severityFilter, ConcurrentSet? suppressedDiagnosticIds) { - return (diagnostic, cancellationToken) => + return (diagnostic, analyzerOptions, cancellationToken) => { var filteredDiagnostic = GetFilteredDiagnostic(diagnostic, compilation, analyzerOptions, severityFilter, suppressedDiagnosticIds, cancellationToken); if (filteredDiagnostic != null) @@ -1974,9 +1981,9 @@ internal static Action GetDiagnosticSink(Action GetDiagnosticSink(Action addLocalDiagnosticCore, Compilation compilation, AnalyzerOptions? analyzerOptions, SeverityFilter severityFilter, ConcurrentSet? suppressedDiagnosticIds) + internal static Action GetDiagnosticSink(Action addLocalDiagnosticCore, Compilation compilation, SeverityFilter severityFilter, ConcurrentSet? suppressedDiagnosticIds) { - return (diagnostic, analyzer, isSyntaxDiagnostic, cancellationToken) => + return (diagnostic, analyzer, analyzerOptions, isSyntaxDiagnostic, cancellationToken) => { var filteredDiagnostic = GetFilteredDiagnostic(diagnostic, compilation, analyzerOptions, severityFilter, suppressedDiagnosticIds, cancellationToken); if (filteredDiagnostic != null) @@ -1986,9 +1993,9 @@ internal static Action }; } - internal static Action GetDiagnosticSink(Action addDiagnosticCore, Compilation compilation, AnalyzerOptions? analyzerOptions, SeverityFilter severityFilter, ConcurrentSet? suppressedDiagnosticIds) + internal static Action GetDiagnosticSink(Action addDiagnosticCore, Compilation compilation, SeverityFilter severityFilter, ConcurrentSet? suppressedDiagnosticIds) { - return (diagnostic, analyzer, cancellationToken) => + return (diagnostic, analyzer, analyzerOptions, cancellationToken) => { var filteredDiagnostic = GetFilteredDiagnostic(diagnostic, compilation, analyzerOptions, severityFilter, suppressedDiagnosticIds, cancellationToken); if (filteredDiagnostic != null) diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerExecutor.AnalyzerDiagnosticReporter.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerExecutor.AnalyzerDiagnosticReporter.cs index f8730fa78dc61..aa593ed94e3d4 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerExecutor.AnalyzerDiagnosticReporter.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerExecutor.AnalyzerDiagnosticReporter.cs @@ -29,10 +29,11 @@ public static AnalyzerDiagnosticReporter GetInstance( TextSpan? span, Compilation compilation, DiagnosticAnalyzer analyzer, + AnalyzerOptions analyzerOptions, bool isSyntaxDiagnostic, - Action? addNonCategorizedDiagnostic, - Action? addCategorizedLocalDiagnostic, - Action? addCategorizedNonLocalDiagnostic, + Action? addNonCategorizedDiagnostic, + Action? addCategorizedLocalDiagnostic, + Action? addCategorizedNonLocalDiagnostic, Func shouldSuppressGeneratedCodeDiagnostic, CancellationToken cancellationToken) { @@ -41,6 +42,7 @@ public static AnalyzerDiagnosticReporter GetInstance( item.FilterSpanForLocalDiagnostics = span; item._compilation = compilation; item._analyzer = analyzer; + item._analyzerOptions = analyzerOptions; item._isSyntaxDiagnostic = isSyntaxDiagnostic; item._addNonCategorizedDiagnostic = addNonCategorizedDiagnostic; item._addCategorizedLocalDiagnostic = addCategorizedLocalDiagnostic; @@ -56,6 +58,7 @@ public void Free() FilterSpanForLocalDiagnostics = null; _compilation = null!; _analyzer = null!; + _analyzerOptions = null!; _isSyntaxDiagnostic = default; _addNonCategorizedDiagnostic = null!; _addCategorizedLocalDiagnostic = null!; @@ -68,10 +71,11 @@ public void Free() private SourceOrAdditionalFile? _contextFile; private Compilation _compilation; private DiagnosticAnalyzer _analyzer; + private AnalyzerOptions _analyzerOptions; private bool _isSyntaxDiagnostic; - private Action? _addNonCategorizedDiagnostic; - private Action? _addCategorizedLocalDiagnostic; - private Action? _addCategorizedNonLocalDiagnostic; + private Action? _addNonCategorizedDiagnostic; + private Action? _addCategorizedLocalDiagnostic; + private Action? _addCategorizedNonLocalDiagnostic; private Func _shouldSuppressGeneratedCodeDiagnostic; private CancellationToken _cancellationToken; @@ -102,7 +106,7 @@ private void AddDiagnostic(Diagnostic diagnostic) if (_addCategorizedLocalDiagnostic == null) { Debug.Assert(_addNonCategorizedDiagnostic != null); - _addNonCategorizedDiagnostic(diagnostic, _cancellationToken); + _addNonCategorizedDiagnostic(diagnostic, _analyzerOptions, _cancellationToken); return; } @@ -112,11 +116,11 @@ private void AddDiagnostic(Diagnostic diagnostic) if (isLocalDiagnostic(diagnostic) && (!FilterSpanForLocalDiagnostics.HasValue || FilterSpanForLocalDiagnostics.Value.IntersectsWith(diagnostic.Location.SourceSpan))) { - _addCategorizedLocalDiagnostic(diagnostic, _analyzer, _isSyntaxDiagnostic, _cancellationToken); + _addCategorizedLocalDiagnostic(diagnostic, _analyzer, _analyzerOptions, _isSyntaxDiagnostic, _cancellationToken); } else { - _addCategorizedNonLocalDiagnostic(diagnostic, _analyzer, _cancellationToken); + _addCategorizedNonLocalDiagnostic(diagnostic, _analyzer, _analyzerOptions, _cancellationToken); } return; diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerExecutor.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerExecutor.cs index 19d913bc9aa2a..897a1161edf38 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerExecutor.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerExecutor.cs @@ -31,9 +31,9 @@ internal partial class AnalyzerExecutor internal const string AnalyzerExceptionDiagnosticId = "AD0001"; internal const string AnalyzerDriverExceptionDiagnosticId = "AD0002"; - private readonly Action? _addNonCategorizedDiagnostic; - private readonly Action? _addCategorizedLocalDiagnostic; - private readonly Action? _addCategorizedNonLocalDiagnostic; + private readonly Action? _addNonCategorizedDiagnostic; + private readonly Action? _addCategorizedLocalDiagnostic; + private readonly Action? _addCategorizedNonLocalDiagnostic; private readonly Action? _addSuppression; private readonly Func? _analyzerExceptionFilter; private readonly AnalyzerManager _analyzerManager; @@ -51,6 +51,17 @@ internal partial class AnalyzerExecutor private readonly ConcurrentDictionary>? _analyzerExecutionTimeMap; private readonly CompilationAnalysisValueProviderFactory _compilationAnalysisValueProviderFactory; + /// + /// Cache of analyzer to analyzer specific options. If there are no specific + /// options, and the shared options should be used for all analyzers. This is the common case, which + /// means we don't pay for the extra indirection of a dictionary lookup normally. + /// + /// Note: this map is generated + /// at construction time, and is unchanging after that point. So it can be safely read from multiple + /// threads without need for locks. + /// + private readonly Dictionary? _analyzerToCachedOptions; + private Func? _lazyGetControlFlowGraph; private ConcurrentDictionary? _lazyControlFlowGraphMap; @@ -79,6 +90,10 @@ private bool IsAnalyzerSuppressedForTree(DiagnosticAnalyzer analyzer, SyntaxTree /// /// Delegate to determine if the given analyzer is compiler analyzer. /// We need to special case the compiler analyzer at few places for performance reasons. + /// Analyzers to query for custom options if is provided. + /// Optional callback to allow individual configuration options + /// on a per analyzer basis. /// Analyzer manager to fetch supported diagnostics. /// /// Delegate to fetch the gate object to guard all callbacks into the analyzer. @@ -98,10 +113,12 @@ private bool IsAnalyzerSuppressedForTree(DiagnosticAnalyzer analyzer, SyntaxTree public static AnalyzerExecutor Create( Compilation compilation, AnalyzerOptions analyzerOptions, - Action? addNonCategorizedDiagnostic, + Action? addNonCategorizedDiagnostic, Action onAnalyzerException, Func? analyzerExceptionFilter, Func isCompilerAnalyzer, + ImmutableArray diagnosticAnalyzers, + Func? getAnalyzerConfigOptionsProvider, AnalyzerManager analyzerManager, Func shouldSkipAnalysisOnGeneratedCode, Func shouldSuppressGeneratedCodeDiagnostic, @@ -111,8 +128,8 @@ public static AnalyzerExecutor Create( Func getSemanticModel, SeverityFilter severityFilter, bool logExecutionTime = false, - Action? addCategorizedLocalDiagnostic = null, - Action? addCategorizedNonLocalDiagnostic = null, + Action? addCategorizedLocalDiagnostic = null, + Action? addCategorizedNonLocalDiagnostic = null, Action? addSuppression = null) { // We can either report categorized (local/non-local) diagnostics or non-categorized diagnostics. @@ -122,7 +139,7 @@ public static AnalyzerExecutor Create( var analyzerExecutionTimeMap = logExecutionTime ? new ConcurrentDictionary>() : null; return new AnalyzerExecutor(compilation, analyzerOptions, addNonCategorizedDiagnostic, onAnalyzerException, analyzerExceptionFilter, - isCompilerAnalyzer, analyzerManager, shouldSkipAnalysisOnGeneratedCode, shouldSuppressGeneratedCodeDiagnostic, isGeneratedCodeLocation, + isCompilerAnalyzer, diagnosticAnalyzers, getAnalyzerConfigOptionsProvider, analyzerManager, shouldSkipAnalysisOnGeneratedCode, shouldSuppressGeneratedCodeDiagnostic, isGeneratedCodeLocation, isAnalyzerSuppressedForTree, getAnalyzerGate, getSemanticModel, severityFilter, analyzerExecutionTimeMap, addCategorizedLocalDiagnostic, addCategorizedNonLocalDiagnostic, addSuppression); } @@ -130,10 +147,12 @@ public static AnalyzerExecutor Create( private AnalyzerExecutor( Compilation compilation, AnalyzerOptions analyzerOptions, - Action? addNonCategorizedDiagnosticOpt, + Action? addNonCategorizedDiagnosticOpt, Action onAnalyzerException, Func? analyzerExceptionFilter, Func isCompilerAnalyzer, + ImmutableArray diagnosticAnalyzers, + Func? getAnalyzerConfigOptionsProvider, AnalyzerManager analyzerManager, Func shouldSkipAnalysisOnGeneratedCode, Func shouldSuppressGeneratedCodeDiagnostic, @@ -143,8 +162,8 @@ private AnalyzerExecutor( Func getSemanticModel, SeverityFilter severityFilter, ConcurrentDictionary>? analyzerExecutionTimeMap, - Action? addCategorizedLocalDiagnostic, - Action? addCategorizedNonLocalDiagnostic, + Action? addCategorizedLocalDiagnostic, + Action? addCategorizedNonLocalDiagnostic, Action? addSuppression) { Compilation = compilation; @@ -167,6 +186,36 @@ private AnalyzerExecutor( _addSuppression = addSuppression; _compilationAnalysisValueProviderFactory = new CompilationAnalysisValueProviderFactory(); + + if (getAnalyzerConfigOptionsProvider != null) + { + var hasDifferentOptions = false; + + var map = new Dictionary( + capacity: diagnosticAnalyzers.Length, ReferenceEqualityComparer.Instance); + + // Deduping map for the distinct AnalyzerConfigOptionsProvider we get back from getAnalyzerConfigOptionsProvider. + // The common case in VS host is that there is generally only 1-2 of these providers. For example, a provider + // that looks in editorconfig+vsoptions, and a provider that only looks in editorconfig. We only want to make + // a corresponding number of AnalyzerOptions instances for each unique provider we see. + var optionsProviderToOptions = new Dictionary(ReferenceEqualityComparer.Instance); + + foreach (var analyzer in diagnosticAnalyzers) + { + var specificOptionsProvider = getAnalyzerConfigOptionsProvider(analyzer); + var specificOptions = optionsProviderToOptions.GetOrAdd( + specificOptionsProvider, () => analyzerOptions.WithAnalyzerConfigOptionsProvider(specificOptionsProvider)); + map[analyzer] = specificOptions; + + if (specificOptions != analyzerOptions) + hasDifferentOptions = true; + } + + // Only if there is at least one analyzer with specific options, we need to maintain the map. + // Otherwise, we can just toss it and use the shared options for all analyzers. + if (hasDifferentOptions) + _analyzerToCachedOptions = map; + } } internal Compilation Compilation { get; } @@ -218,13 +267,17 @@ public void ExecuteInitializeMethod(HostSessionStartAnalysisScope sessionScope, public void ExecuteCompilationStartActions(ImmutableArray actions, HostCompilationStartAnalysisScope compilationScope, CancellationToken cancellationToken) { // This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we - // can use the same instance across all actions. + // can use the same instance across all actions as long as the same options are picked for each analyzer. var context = new AnalyzerCompilationStartAnalysisContext(compilationScope, Compilation, AnalyzerOptions, _compilationAnalysisValueProviderFactory, cancellationToken); var contextInfo = new AnalysisContextInfo(Compilation); foreach (var startAction in actions) { + // See if we need to use an analyzer specific options instance. + context = WithAnalyzerSpecificOptions( + context, startAction.Analyzer, static (context, options) => context.WithOptions(options)); + ExecuteAndCatchIfThrows( startAction.Analyzer, static data => data.startAction.Action(data.context), @@ -234,6 +287,24 @@ public void ExecuteCompilationStartActions(ImmutableArray( + TAnalysisContext context, + DiagnosticAnalyzer analyzer, + Func withOptions) + { + // No specific options factory. Can use the shared context. + if (_analyzerToCachedOptions is null) + return context; + + return withOptions(context, GetAnalyzerSpecificOptions(analyzer)); + } + + /// + /// Given an analyzer, returns any specific options for it, or the shared options if none. + /// + private AnalyzerOptions GetAnalyzerSpecificOptions(DiagnosticAnalyzer analyzer) + => _analyzerToCachedOptions?[analyzer] ?? AnalyzerOptions; + /// /// Executes the symbol start actions. /// @@ -258,7 +329,7 @@ public void ExecuteSymbolStartActions( } // This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we - // can use the same instance across all actions. + // can use the same instance across all actions (as long as the options stay the same per analyzer). var context = new AnalyzerSymbolStartAnalysisContext(symbolScope, symbol, Compilation, AnalyzerOptions, isGeneratedCodeSymbol, filterTree, filterSpan, cancellationToken); var contextInfo = new AnalysisContextInfo(Compilation, symbol); @@ -267,6 +338,10 @@ public void ExecuteSymbolStartActions( { Debug.Assert(startAction.Analyzer == symbolScope.Analyzer); + // See if we need to use an analyzer specific options instance. + context = WithAnalyzerSpecificOptions( + context, startAction.Analyzer, static (context, options) => context.WithOptions(options)); + ExecuteAndCatchIfThrows( startAction.Analyzer, static data => data.startAction.Action(data.context), @@ -300,7 +375,8 @@ public void ExecuteSuppressionAction(DiagnosticSuppressor suppressor, ImmutableA supportedSuppressions, out Func isSupportedSuppression); - var context = new SuppressionAnalysisContext(Compilation, AnalyzerOptions, + var options = GetAnalyzerSpecificOptions(suppressor); + var context = new SuppressionAnalysisContext(Compilation, options, reportedDiagnostics, _addSuppression, isSupportedSuppression, _getSemanticModel, cancellationToken); ExecuteAndCatchIfThrows( @@ -326,7 +402,8 @@ public void ExecuteCompilationActions( { Debug.Assert(compilationEvent is CompilationStartedEvent || compilationEvent is CompilationCompletedEvent); - var addDiagnostic = GetAddCompilationDiagnostic(analyzer, cancellationToken); + var analyzerOptions = this.GetAnalyzerSpecificOptions(analyzer); + var addDiagnostic = GetAddCompilationDiagnostic(analyzer, analyzerOptions, cancellationToken); using var _ = PooledDelegates.GetPooledFunction( static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct), @@ -336,7 +413,7 @@ public void ExecuteCompilationActions( // This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we // can use the same instance across all actions. var context = new CompilationAnalysisContext( - Compilation, AnalyzerOptions, addDiagnostic, + Compilation, analyzerOptions, addDiagnostic, isSupportedDiagnostic, _compilationAnalysisValueProviderFactory, cancellationToken); var contextInfo = new AnalysisContextInfo(Compilation); @@ -380,7 +457,9 @@ public void ExecuteSymbolActions( } var symbol = symbolDeclaredEvent.Symbol; - var addDiagnostic = GetAddDiagnostic(symbol, symbolDeclaredEvent.DeclaringSyntaxReferences, analyzer, getTopMostNodeForAnalysis, cancellationToken); + var analyzerOptions = this.GetAnalyzerSpecificOptions(analyzer); + var addDiagnostic = GetAddDiagnostic( + symbol, symbolDeclaredEvent.DeclaringSyntaxReferences, analyzer, analyzerOptions, getTopMostNodeForAnalysis, cancellationToken); using var _ = PooledDelegates.GetPooledFunction( static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct), @@ -390,7 +469,7 @@ public void ExecuteSymbolActions( // This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we // can use the same instance across all actions. var context = new SymbolAnalysisContext( - symbol, Compilation, AnalyzerOptions, addDiagnostic, + symbol, Compilation, analyzerOptions, addDiagnostic, isSupportedDiagnostic, isGeneratedCodeSymbol, filterTree, filterSpan, cancellationToken); var contextInfo = new AnalysisContextInfo(Compilation, symbol); @@ -490,7 +569,8 @@ private void ExecuteSymbolEndActionsCore( Debug.Assert(!filterSpan.HasValue || filterTree != null); var symbol = symbolDeclaredEvent.Symbol; - var addDiagnostic = GetAddDiagnostic(symbol, symbolDeclaredEvent.DeclaringSyntaxReferences, analyzer, getTopMostNodeForAnalysis, cancellationToken); + var analyzerOptions = this.GetAnalyzerSpecificOptions(analyzer); + var addDiagnostic = GetAddDiagnostic(symbol, symbolDeclaredEvent.DeclaringSyntaxReferences, analyzer, analyzerOptions, getTopMostNodeForAnalysis, cancellationToken); using var _ = PooledDelegates.GetPooledFunction( static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct), @@ -499,7 +579,8 @@ private void ExecuteSymbolEndActionsCore( // This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we // can use the same instance across all actions. - var context = new SymbolAnalysisContext(symbol, Compilation, AnalyzerOptions, addDiagnostic, + var context = new SymbolAnalysisContext( + symbol, Compilation, analyzerOptions, addDiagnostic, isSupportedDiagnostic, isGeneratedCode, filterTree, filterSpan, cancellationToken); var contextInfo = new AnalysisContextInfo(Compilation, symbol); @@ -539,7 +620,8 @@ public void ExecuteSemanticModelActions( return; } - var diagReporter = GetAddSemanticDiagnostic(semanticModel.SyntaxTree, analyzer, cancellationToken); + var analyzerOptions = this.GetAnalyzerSpecificOptions(analyzer); + var diagReporter = GetAddSemanticDiagnostic(semanticModel.SyntaxTree, analyzer, analyzerOptions, cancellationToken); using var _ = PooledDelegates.GetPooledFunction( static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct), @@ -549,7 +631,7 @@ public void ExecuteSemanticModelActions( // This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we // can use the same instance across all actions. var context = new SemanticModelAnalysisContext( - semanticModel, AnalyzerOptions, diagReporter.AddDiagnosticAction, + semanticModel, analyzerOptions, diagReporter.AddDiagnosticAction, isSupportedDiagnostic, filterSpan, isGeneratedCode, cancellationToken); var contextInfo = new AnalysisContextInfo(semanticModel); @@ -592,7 +674,8 @@ public void ExecuteSyntaxTreeActions( return; } - var diagReporter = GetAddSyntaxDiagnostic(file, analyzer, cancellationToken); + var analyzerOptions = this.GetAnalyzerSpecificOptions(analyzer); + var diagReporter = GetAddSyntaxDiagnostic(file, analyzer, analyzerOptions, cancellationToken); using var _ = PooledDelegates.GetPooledFunction( static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct), @@ -602,7 +685,7 @@ public void ExecuteSyntaxTreeActions( // This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we // can use the same instance across all actions. var context = new SyntaxTreeAnalysisContext( - tree, AnalyzerOptions, diagReporter.AddDiagnosticAction, isSupportedDiagnostic, + tree, analyzerOptions, diagReporter.AddDiagnosticAction, isSupportedDiagnostic, Compilation, filterSpan, isGeneratedCode, cancellationToken); var contextInfo = new AnalysisContextInfo(Compilation, file); @@ -637,7 +720,8 @@ public void ExecuteAdditionalFileActions( Debug.Assert(file.AdditionalFile != null); var additionalFile = file.AdditionalFile; - var diagReporter = GetAddSyntaxDiagnostic(file, analyzer, cancellationToken); + var analyzerOptions = this.GetAnalyzerSpecificOptions(analyzer); + var diagReporter = GetAddSyntaxDiagnostic(file, analyzer, analyzerOptions, cancellationToken); using var _ = PooledDelegates.GetPooledFunction( static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct), @@ -647,7 +731,7 @@ public void ExecuteAdditionalFileActions( // This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we // can use the same instance across all actions. var context = new AdditionalFileAnalysisContext( - additionalFile, AnalyzerOptions, diagReporter.AddDiagnosticAction, isSupportedDiagnostic, + additionalFile, analyzerOptions, diagReporter.AddDiagnosticAction, isSupportedDiagnostic, Compilation, filterSpan, cancellationToken); var contextInfo = new AnalysisContextInfo(Compilation, file); @@ -677,7 +761,8 @@ private void ExecuteSyntaxNodeAction( Debug.Assert(!IsAnalyzerSuppressedForTree(syntaxNodeAction.Analyzer, node.SyntaxTree, cancellationToken)); var syntaxNodeContext = new SyntaxNodeAnalysisContext( - node, executionData.DeclaredSymbol, executionData.SemanticModel, AnalyzerOptions, addDiagnostic, + node, executionData.DeclaredSymbol, executionData.SemanticModel, + GetAnalyzerSpecificOptions(syntaxNodeAction.Analyzer), addDiagnostic, isSupportedDiagnostic, executionData.FilterSpan, executionData.IsGeneratedCode, cancellationToken); ExecuteAndCatchIfThrows( @@ -701,7 +786,7 @@ private void ExecuteOperationAction( var operationContext = new OperationAnalysisContext( operation, executionData.DeclaredSymbol, executionData.SemanticModel.Compilation, - AnalyzerOptions, addDiagnostic, isSupportedDiagnostic, GetControlFlowGraph, + GetAnalyzerSpecificOptions(operationAction.Analyzer), addDiagnostic, isSupportedDiagnostic, GetControlFlowGraph, executionData.FilterSpan, executionData.IsGeneratedCode, cancellationToken); ExecuteAndCatchIfThrows( @@ -714,12 +799,14 @@ private void ExecuteOperationAction( private readonly struct ExecutionData( DiagnosticAnalyzer analyzer, + AnalyzerOptions analyzerOptions, ISymbol declaredSymbol, SemanticModel semanticModel, TextSpan? filterSpan, bool isGeneratedCode) { public readonly DiagnosticAnalyzer Analyzer = analyzer; + public readonly AnalyzerOptions AnalyzerOptions = analyzerOptions; public readonly ISymbol DeclaredSymbol = declaredSymbol; public readonly SemanticModel SemanticModel = semanticModel; public readonly TextSpan? FilterSpan = filterSpan; @@ -748,12 +835,13 @@ public void ExecuteCodeBlockActions( // The actions we discover in 'addActions' and then execute in 'executeActions'. var ephemeralActions = ArrayBuilder>.GetInstance(); + var analyzerOptions = this.GetAnalyzerSpecificOptions(analyzer); ExecuteBlockActionsCore( startActions, actions, endActions, declaredNode, - new ExecutionData(analyzer, declaredSymbol, semanticModel, filterSpan, isGeneratedCode), + new ExecutionData(analyzer, analyzerOptions, declaredSymbol, semanticModel, filterSpan, isGeneratedCode), addActions: static (startAction, endActions, executionData, args, cancellationToken) => { var (@this, startActions, executableCodeBlocks, declaredNode, getKind, ephemeralActions) = args; @@ -761,7 +849,7 @@ public void ExecuteCodeBlockActions( var scope = new HostCodeBlockStartAnalysisScope(startAction.Analyzer); var startContext = new AnalyzerCodeBlockStartAnalysisContext( scope, declaredNode, executionData.DeclaredSymbol, executionData.SemanticModel, - @this.AnalyzerOptions, executionData.FilterSpan, executionData.IsGeneratedCode, cancellationToken); + executionData.AnalyzerOptions, executionData.FilterSpan, executionData.IsGeneratedCode, cancellationToken); // Catch Exception from the start action. @this.ExecuteAndCatchIfThrows( @@ -812,7 +900,7 @@ public void ExecuteCodeBlockActions( var (@this, startActions, executableCodeBlocks, declaredNode, getKind, ephemeralActions) = args; var context = new CodeBlockAnalysisContext(declaredNode, executionData.DeclaredSymbol, executionData.SemanticModel, - @this.AnalyzerOptions, diagReporter.AddDiagnosticAction, isSupportedDiagnostic, executionData.FilterSpan, executionData.IsGeneratedCode, cancellationToken); + executionData.AnalyzerOptions, diagReporter.AddDiagnosticAction, isSupportedDiagnostic, executionData.FilterSpan, executionData.IsGeneratedCode, cancellationToken); foreach (var blockAction in blockActions) { @@ -850,18 +938,20 @@ public void ExecuteOperationBlockActions( // The actions we discover in 'addActions' and then execute in 'executeActions'. var ephemeralActions = ArrayBuilder.GetInstance(); + + var analyzerOptions = this.GetAnalyzerSpecificOptions(analyzer); ExecuteBlockActionsCore( startActions, actions, endActions, declaredNode, - new ExecutionData(analyzer, declaredSymbol, semanticModel, filterSpan, isGeneratedCode), + new ExecutionData(analyzer, analyzerOptions, declaredSymbol, semanticModel, filterSpan, isGeneratedCode), addActions: static (startAction, endActions, executionData, args, cancellationToken) => { var (@this, startActions, declaredNode, operationBlocks, operations, ephemeralActions) = args; var scope = new HostOperationBlockStartAnalysisScope(startAction.Analyzer); var startContext = new AnalyzerOperationBlockStartAnalysisContext( - scope, operationBlocks, executionData.DeclaredSymbol, executionData.SemanticModel.Compilation, @this.AnalyzerOptions, + scope, operationBlocks, executionData.DeclaredSymbol, executionData.SemanticModel.Compilation, executionData.AnalyzerOptions, @this.GetControlFlowGraph, declaredNode.SyntaxTree, executionData.FilterSpan, executionData.IsGeneratedCode, cancellationToken); // Catch Exception from the start action. @@ -892,7 +982,7 @@ public void ExecuteOperationBlockActions( var (@this, startActions, declaredNode, operationBlocks, operations, ephemeralActions) = args; var context = new OperationBlockAnalysisContext(operationBlocks, executionData.DeclaredSymbol, @this.Compilation, - @this.AnalyzerOptions, diagReporter.AddDiagnosticAction, isSupportedDiagnostic, @this.GetControlFlowGraph, declaredNode.SyntaxTree, + executionData.AnalyzerOptions, diagReporter.AddDiagnosticAction, isSupportedDiagnostic, @this.GetControlFlowGraph, declaredNode.SyntaxTree, executionData.FilterSpan, executionData.IsGeneratedCode, cancellationToken); foreach (var blockAction in blockActions) @@ -948,7 +1038,8 @@ private void ExecuteBlockActionsCore( blockEndActions.AddAll(endActions); var diagReporter = GetAddSemanticDiagnostic( - executionData.SemanticModel.SyntaxTree, declaredNode.FullSpan, executionData.Analyzer, cancellationToken); + executionData.SemanticModel.SyntaxTree, declaredNode.FullSpan, + executionData.Analyzer, executionData.AnalyzerOptions, cancellationToken); // Include the stateful actions. foreach (var startAction in startActions) @@ -1012,7 +1103,9 @@ public void ExecuteSyntaxNodeActions( return; } - var diagReporter = GetAddSemanticDiagnostic(model.SyntaxTree, spanForContainingTopmostNodeForAnalysis, analyzer, cancellationToken); + var analyzerOptions = this.GetAnalyzerSpecificOptions(analyzer); + var diagReporter = GetAddSemanticDiagnostic( + model.SyntaxTree, spanForContainingTopmostNodeForAnalysis, analyzer, analyzerOptions, cancellationToken); using var _ = PooledDelegates.GetPooledFunction( static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct), @@ -1021,7 +1114,7 @@ public void ExecuteSyntaxNodeActions( ExecuteSyntaxNodeActions( nodesToAnalyze, nodeActionsByKind, - new ExecutionData(analyzer, declaredSymbol, model, filterSpan, isGeneratedCode), + new ExecutionData(analyzer, analyzerOptions, declaredSymbol, model, filterSpan, isGeneratedCode), getKind, diagReporter, isSupportedDiagnostic, hasCodeBlockStartOrSymbolStartActions, cancellationToken); diagReporter.Free(); @@ -1111,7 +1204,9 @@ public void ExecuteOperationActions( return; } - var diagReporter = GetAddSemanticDiagnostic(model.SyntaxTree, spanForContainingOperationBlock, analyzer, cancellationToken); + var analyzerOptions = this.GetAnalyzerSpecificOptions(analyzer); + var diagReporter = GetAddSemanticDiagnostic( + model.SyntaxTree, spanForContainingOperationBlock, analyzer, analyzerOptions, cancellationToken); using var _ = PooledDelegates.GetPooledFunction( static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct), @@ -1120,7 +1215,7 @@ public void ExecuteOperationActions( ExecuteOperationActions( operationsToAnalyze, operationActionsByKind, - new ExecutionData(analyzer, declaredSymbol, model, filterSpan, isGeneratedCode), + new ExecutionData(analyzer, analyzerOptions, declaredSymbol, model, filterSpan, isGeneratedCode), diagReporter, isSupportedDiagnostic, hasOperationBlockStartOrSymbolStartActions, cancellationToken); diagReporter.Free(); @@ -1412,10 +1507,26 @@ private bool IsSupportedDiagnostic(DiagnosticAnalyzer analyzer, Diagnostic diagn return _analyzerManager.IsSupportedDiagnostic(analyzer, diagnostic, _isCompilerAnalyzer, this, cancellationToken); } - private Action GetAddDiagnostic(ISymbol contextSymbol, ImmutableArray cachedDeclaringReferences, DiagnosticAnalyzer analyzer, Func getTopMostNodeForAnalysis, CancellationToken cancellationToken) + private Action GetAddDiagnostic( + ISymbol contextSymbol, + ImmutableArray cachedDeclaringReferences, + DiagnosticAnalyzer analyzer, + AnalyzerOptions options, + Func getTopMostNodeForAnalysis, + CancellationToken cancellationToken) { - return GetAddDiagnostic(contextSymbol, cachedDeclaringReferences, Compilation, analyzer, _addNonCategorizedDiagnostic, - _addCategorizedLocalDiagnostic, _addCategorizedNonLocalDiagnostic, getTopMostNodeForAnalysis, _shouldSuppressGeneratedCodeDiagnostic, cancellationToken); + return GetAddDiagnostic( + contextSymbol, + cachedDeclaringReferences, + Compilation, + analyzer, + options, + _addNonCategorizedDiagnostic, + _addCategorizedLocalDiagnostic, + _addCategorizedNonLocalDiagnostic, + getTopMostNodeForAnalysis, + _shouldSuppressGeneratedCodeDiagnostic, + cancellationToken); } private static Action GetAddDiagnostic( @@ -1423,9 +1534,10 @@ private static Action GetAddDiagnostic( ImmutableArray cachedDeclaringReferences, Compilation compilation, DiagnosticAnalyzer analyzer, - Action? addNonCategorizedDiagnostic, - Action? addCategorizedLocalDiagnostic, - Action? addCategorizedNonLocalDiagnostic, + AnalyzerOptions options, + Action? addNonCategorizedDiagnostic, + Action? addCategorizedLocalDiagnostic, + Action? addCategorizedNonLocalDiagnostic, Func getTopMostNodeForAnalysis, Func shouldSuppressGeneratedCodeDiagnostic, CancellationToken cancellationToken) @@ -1440,7 +1552,7 @@ private static Action GetAddDiagnostic( if (addCategorizedLocalDiagnostic == null) { Debug.Assert(addNonCategorizedDiagnostic != null); - addNonCategorizedDiagnostic(diagnostic, cancellationToken); + addNonCategorizedDiagnostic(diagnostic, options, cancellationToken); return; } @@ -1456,18 +1568,21 @@ private static Action GetAddDiagnostic( var syntax = getTopMostNodeForAnalysis(contextSymbol, syntaxRef, compilation, cancellationToken); if (diagnostic.Location.SourceSpan.IntersectsWith(syntax.FullSpan)) { - addCategorizedLocalDiagnostic(diagnostic, analyzer, false, cancellationToken); + addCategorizedLocalDiagnostic(diagnostic, analyzer, options, false, cancellationToken); return; } } } } - addCategorizedNonLocalDiagnostic(diagnostic, analyzer, cancellationToken); + addCategorizedNonLocalDiagnostic(diagnostic, analyzer, options, cancellationToken); }; } - private Action GetAddCompilationDiagnostic(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) + private Action GetAddCompilationDiagnostic( + DiagnosticAnalyzer analyzer, + AnalyzerOptions analyzerOptions, + CancellationToken cancellationToken) { return diagnostic => { @@ -1479,31 +1594,47 @@ private Action GetAddCompilationDiagnostic(DiagnosticAnalyzer analyz if (_addCategorizedNonLocalDiagnostic == null) { Debug.Assert(_addNonCategorizedDiagnostic != null); - _addNonCategorizedDiagnostic(diagnostic, cancellationToken); + _addNonCategorizedDiagnostic(diagnostic, analyzerOptions, cancellationToken); return; } - _addCategorizedNonLocalDiagnostic(diagnostic, analyzer, cancellationToken); + _addCategorizedNonLocalDiagnostic(diagnostic, analyzer, analyzerOptions, cancellationToken); }; } - private AnalyzerDiagnosticReporter GetAddSemanticDiagnostic(SyntaxTree tree, DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) + private AnalyzerDiagnosticReporter GetAddSemanticDiagnostic( + SyntaxTree tree, + DiagnosticAnalyzer analyzer, + AnalyzerOptions analyzerOptions, + CancellationToken cancellationToken) { - return AnalyzerDiagnosticReporter.GetInstance(new SourceOrAdditionalFile(tree), span: null, Compilation, analyzer, isSyntaxDiagnostic: false, + return AnalyzerDiagnosticReporter.GetInstance( + new SourceOrAdditionalFile(tree), span: null, Compilation, analyzer, analyzerOptions, isSyntaxDiagnostic: false, _addNonCategorizedDiagnostic, _addCategorizedLocalDiagnostic, _addCategorizedNonLocalDiagnostic, _shouldSuppressGeneratedCodeDiagnostic, cancellationToken); } - private AnalyzerDiagnosticReporter GetAddSemanticDiagnostic(SyntaxTree tree, TextSpan? span, DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) + private AnalyzerDiagnosticReporter GetAddSemanticDiagnostic( + SyntaxTree tree, + TextSpan? span, + DiagnosticAnalyzer analyzer, + AnalyzerOptions analyzerOptions, + CancellationToken cancellationToken) { - return AnalyzerDiagnosticReporter.GetInstance(new SourceOrAdditionalFile(tree), span, Compilation, analyzer, isSyntaxDiagnostic: false, + return AnalyzerDiagnosticReporter.GetInstance( + new SourceOrAdditionalFile(tree), span, Compilation, analyzer, analyzerOptions, isSyntaxDiagnostic: false, _addNonCategorizedDiagnostic, _addCategorizedLocalDiagnostic, _addCategorizedNonLocalDiagnostic, _shouldSuppressGeneratedCodeDiagnostic, cancellationToken); } - private AnalyzerDiagnosticReporter GetAddSyntaxDiagnostic(SourceOrAdditionalFile file, DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) + private AnalyzerDiagnosticReporter GetAddSyntaxDiagnostic( + SourceOrAdditionalFile file, + DiagnosticAnalyzer analyzer, + AnalyzerOptions analyzerOptions, + CancellationToken cancellationToken) { - return AnalyzerDiagnosticReporter.GetInstance(file, span: null, Compilation, analyzer, isSyntaxDiagnostic: true, + return AnalyzerDiagnosticReporter.GetInstance( + file, span: null, Compilation, analyzer, analyzerOptions, isSyntaxDiagnostic: true, _addNonCategorizedDiagnostic, _addCategorizedLocalDiagnostic, _addCategorizedNonLocalDiagnostic, _shouldSuppressGeneratedCodeDiagnostic, cancellationToken); } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerOptions.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerOptions.cs index 0b34cc33b77e9..a79219b0a37c5 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerOptions.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerOptions.cs @@ -63,6 +63,11 @@ public AnalyzerOptions WithAdditionalFiles(ImmutableArray additi return new AnalyzerOptions(additionalFiles); } + internal AnalyzerOptions WithAnalyzerConfigOptionsProvider(AnalyzerConfigOptionsProvider optionsProvider) + => this.AnalyzerConfigOptionsProvider == optionsProvider + ? this + : new(this.AdditionalFiles, optionsProvider); + public override bool Equals(object? obj) { if (ReferenceEquals(this, obj)) diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerOptionsExtensions.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerOptionsExtensions.cs index e7b8ac7b8f72d..62b28f20ac7df 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerOptionsExtensions.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerOptionsExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Immutable; using System.Threading; using Microsoft.CodeAnalysis.InternalUtilities; @@ -19,6 +20,9 @@ internal static class AnalyzerOptionsExtensions private static string GetCategoryBasedDotnetAnalyzerDiagnosticSeverityKey(string category) => s_categoryToSeverityKeyMap.GetOrAdd(category, category, static category => $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{category}.{SeveritySuffix}"); + public static ImmutableArray GetAdditionalFiles(this AnalyzerOptions? analyzerOptions) + => analyzerOptions?.AdditionalFiles ?? []; + /// /// Tries to get configured severity for the given /// for the given from bulk configuration analyzer config options, i.e. diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilationWithAnalyzers.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilationWithAnalyzers.cs index 67c47debe0336..c3a5afae49e2b 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilationWithAnalyzers.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilationWithAnalyzers.cs @@ -104,7 +104,7 @@ public CompilationWithAnalyzers(Compilation compilation, ImmutableArray().ToImmutableArrayOrEmpty(); _analysisOptions = analysisOptions; - _analysisResultBuilder = new AnalysisResultBuilder(analysisOptions.LogAnalyzerExecutionTime, analyzers, _analysisOptions.Options?.AdditionalFiles ?? ImmutableArray.Empty); + _analysisResultBuilder = new AnalysisResultBuilder(analysisOptions.LogAnalyzerExecutionTime, analyzers, _analysisOptions.Options.GetAdditionalFiles()); _compilationAnalysisScope = AnalysisScope.Create(_compilation, _analyzers, this); } @@ -224,7 +224,7 @@ private void VerifyAdditionalFile(AdditionalText file) #endregion - private ImmutableArray AdditionalFiles => _analysisOptions.Options?.AdditionalFiles ?? ImmutableArray.Empty; + private ImmutableArray AdditionalFiles => _analysisOptions.Options.GetAdditionalFiles(); /// /// Returns diagnostics produced by all . diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilationWithAnalyzersOptions.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilationWithAnalyzersOptions.cs index 015cf46f382a8..33cf088af400e 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilationWithAnalyzersOptions.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilationWithAnalyzersOptions.cs @@ -48,6 +48,13 @@ public sealed class CompilationWithAnalyzersOptions /// public bool ReportSuppressedDiagnostics => _reportSuppressedDiagnostics; + /// + /// Callback to allow individual analyzers to have their own + /// distinct from the shared instance provided in . If then will be used for all analyzers. + /// + internal readonly Func? GetAnalyzerConfigOptionsProvider; + /// /// Creates a new . /// @@ -98,6 +105,23 @@ public CompilationWithAnalyzersOptions( bool logAnalyzerExecutionTime, bool reportSuppressedDiagnostics, Func? analyzerExceptionFilter) + : this(options, onAnalyzerException, concurrentAnalysis, logAnalyzerExecutionTime, reportSuppressedDiagnostics, analyzerExceptionFilter, getAnalyzerConfigOptionsProvider: null) + { + } + + /// + /// Callback to allow individual analyzers to have their own distinct from the shared instance provided in . If then will be used for all + /// analyzers. + public CompilationWithAnalyzersOptions( + AnalyzerOptions? options, + Action? onAnalyzerException, + bool concurrentAnalysis, + bool logAnalyzerExecutionTime, + bool reportSuppressedDiagnostics, + Func? analyzerExceptionFilter, + Func? getAnalyzerConfigOptionsProvider) { _options = options; _onAnalyzerException = onAnalyzerException; @@ -105,6 +129,7 @@ public CompilationWithAnalyzersOptions( _concurrentAnalysis = concurrentAnalysis; _logAnalyzerExecutionTime = logAnalyzerExecutionTime; _reportSuppressedDiagnostics = reportSuppressedDiagnostics; + this.GetAnalyzerConfigOptionsProvider = getAnalyzerConfigOptionsProvider; } } } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticStartAnalysisScope.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticStartAnalysisScope.cs index db530e231f9bf..9fa5b256030cc 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticStartAnalysisScope.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticStartAnalysisScope.cs @@ -215,6 +215,11 @@ internal override bool TryGetValueCore(TKey key, AnalysisValueProv var compilationAnalysisValueProvider = _compilationAnalysisValueProviderFactory.GetValueProvider(valueProvider); return compilationAnalysisValueProvider.TryGetValue(key, out value); } + + public AnalyzerCompilationStartAnalysisContext WithOptions(AnalyzerOptions options) + => this.Options == options + ? this + : new(_scope, this.Compilation, options, _compilationAnalysisValueProviderFactory, this.CancellationToken); } /// @@ -279,6 +284,11 @@ public override void RegisterOperationAction(Action ac DiagnosticAnalysisContextHelpers.VerifyArguments(action, operationKinds); _scope.RegisterOperationAction(action, operationKinds); } + + public AnalyzerSymbolStartAnalysisContext WithOptions(AnalyzerOptions analyzerOptions) + => this.Options == analyzerOptions + ? this + : new(_scope, this.Symbol, this.Compilation, analyzerOptions, this.IsGeneratedCode, this.FilterTree, this.FilterSpan, this.CancellationToken); } /// diff --git a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt index 47f127762ce1e..cf2341a97a094 100644 --- a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt @@ -8,6 +8,7 @@ Microsoft.CodeAnalysis.CommandLineResource.IsPublic.get -> bool Microsoft.CodeAnalysis.CommandLineResource.LinkedResourceFileName.get -> string? Microsoft.CodeAnalysis.CommandLineResource.ResourceName.get -> string! Microsoft.CodeAnalysis.Compilation.EmitDifference(Microsoft.CodeAnalysis.Emit.EmitBaseline! baseline, System.Collections.Generic.IEnumerable! edits, System.Func! isAddedSymbol, System.IO.Stream! metadataStream, System.IO.Stream! ilStream, System.IO.Stream! pdbStream, Microsoft.CodeAnalysis.Emit.EmitDifferenceOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.CodeAnalysis.Emit.EmitDifferenceResult! +Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzersOptions.CompilationWithAnalyzersOptions(Microsoft.CodeAnalysis.Diagnostics.AnalyzerOptions? options, System.Action? onAnalyzerException, bool concurrentAnalysis, bool logAnalyzerExecutionTime, bool reportSuppressedDiagnostics, System.Func? analyzerExceptionFilter, System.Func? getAnalyzerConfigOptionsProvider) -> void Microsoft.CodeAnalysis.Emit.EmitDifferenceOptions Microsoft.CodeAnalysis.Emit.EmitDifferenceOptions.EmitDifferenceOptions() -> void Microsoft.CodeAnalysis.Emit.EmitDifferenceOptions.EmitFieldRva.get -> bool diff --git a/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/Microsoft.CodeAnalysis.txt b/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/Microsoft.CodeAnalysis.txt index 6d660b77c0104..d6270344b5753 100644 --- a/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/Microsoft.CodeAnalysis.txt +++ b/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/Microsoft.CodeAnalysis.txt @@ -686,6 +686,7 @@ Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzersOptions Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzersOptions.#ctor(Microsoft.CodeAnalysis.Diagnostics.AnalyzerOptions,System.Action{System.Exception,Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer,Microsoft.CodeAnalysis.Diagnostic},System.Boolean,System.Boolean) Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzersOptions.#ctor(Microsoft.CodeAnalysis.Diagnostics.AnalyzerOptions,System.Action{System.Exception,Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer,Microsoft.CodeAnalysis.Diagnostic},System.Boolean,System.Boolean,System.Boolean) Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzersOptions.#ctor(Microsoft.CodeAnalysis.Diagnostics.AnalyzerOptions,System.Action{System.Exception,Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer,Microsoft.CodeAnalysis.Diagnostic},System.Boolean,System.Boolean,System.Boolean,System.Func{System.Exception,System.Boolean}) +Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzersOptions.#ctor(Microsoft.CodeAnalysis.Diagnostics.AnalyzerOptions,System.Action{System.Exception,Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer,Microsoft.CodeAnalysis.Diagnostic},System.Boolean,System.Boolean,System.Boolean,System.Func{System.Exception,System.Boolean},System.Func{Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer,Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptionsProvider}) Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzersOptions.get_AnalyzerExceptionFilter Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzersOptions.get_ConcurrentAnalysis Microsoft.CodeAnalysis.Diagnostics.CompilationWithAnalyzersOptions.get_LogAnalyzerExecutionTime