Skip to content

Commit 906c266

Browse files
Add api to allow customizing options per DiagnosticAnalyzer (#80200)
2 parents 87285c1 + 1c82871 commit 906c266

File tree

13 files changed

+493
-97
lines changed

13 files changed

+493
-97
lines changed

src/Compilers/CSharp/Test/Emit3/Diagnostics/DiagnosticAnalyzerTests.cs

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using Microsoft.CodeAnalysis.Test.Utilities;
2424
using Microsoft.CodeAnalysis.Text;
2525
using Roslyn.Test.Utilities;
26+
using Roslyn.Test.Utilities.TestGenerators;
2627
using Roslyn.Utilities;
2728
using Xunit;
2829

@@ -4441,5 +4442,203 @@ record B(int I) : A(I);
44414442
var diagnostics = await compWithAnalyzers.GetAnalyzerSemanticDiagnosticsAsync(model, filterSpan: null, CancellationToken.None);
44424443
diagnostics.Verify(Diagnostic("ID0001", "B").WithLocation(1, 8));
44434444
}
4445+
4446+
private sealed class OptionsOverrideDiagnosticAnalyzer(AnalyzerConfigOptionsProvider customOptions) : DiagnosticAnalyzer
4447+
{
4448+
private static readonly DiagnosticDescriptor s_descriptor = new DiagnosticDescriptor(
4449+
id: "ID0001",
4450+
title: "Title",
4451+
messageFormat: "Message",
4452+
category: "Category",
4453+
defaultSeverity: DiagnosticSeverity.Warning,
4454+
isEnabledByDefault: true);
4455+
4456+
private readonly AnalyzerConfigOptionsProvider _customOptions = customOptions;
4457+
4458+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [s_descriptor];
4459+
4460+
public bool RegisterAdditionalFileActionInvoked { get; private set; }
4461+
public bool RegisterCodeBlockActionInvoked { get; private set; }
4462+
public bool RegisterCodeBlockStartActionInvoked { get; private set; }
4463+
public bool RegisterCompilationActionInvoked { get; private set; }
4464+
public bool RegisterOperationActionInvoked { get; private set; }
4465+
public bool RegisterOperationBlockActionInvoked { get; private set; }
4466+
public bool RegisterSemanticModelActionInvoked { get; private set; }
4467+
public bool RegisterSymbolActionInvoked { get; private set; }
4468+
public bool RegisterSyntaxNodeActionInvoked { get; private set; }
4469+
public bool RegisterSyntaxTreeActionInvoked { get; private set; }
4470+
4471+
public bool RegisterOperationBlockStartActionInvoked { get; private set; }
4472+
public bool RegisterOperationBlockEndActionInvoked { get; private set; }
4473+
public bool RegisterCompilationStartActionInvoked { get; private set; }
4474+
public bool RegisterCompilationEndActionInvoked { get; private set; }
4475+
public bool RegisterSymbolStartActionInvoked { get; private set; }
4476+
public bool RegisterSymbolEndActionInvoked { get; private set; }
4477+
4478+
public AnalyzerOptions SeenOptions;
4479+
4480+
private void AssertSame(AnalyzerOptions options)
4481+
{
4482+
// First, assert that the options provider we see is the custom one the test sets.
4483+
Assert.Same(options.AnalyzerConfigOptionsProvider, _customOptions);
4484+
4485+
if (SeenOptions is null)
4486+
SeenOptions = options;
4487+
4488+
// Also ensure that the compiler actually passes the same AnalyzerOptions wrapper around
4489+
// the options provider. That ensures we're not accidentally creating new instances unnecessarily.
4490+
Assert.Same(SeenOptions, options);
4491+
}
4492+
4493+
public override void Initialize(AnalysisContext context)
4494+
{
4495+
context.RegisterAdditionalFileAction(context => { AssertSame(context.Options); RegisterAdditionalFileActionInvoked = true; });
4496+
context.RegisterCodeBlockAction(context => { AssertSame(context.Options); RegisterCodeBlockActionInvoked = true; });
4497+
context.RegisterCodeBlockStartAction<SyntaxKind>(context => { AssertSame(context.Options); RegisterCodeBlockStartActionInvoked = true; });
4498+
context.RegisterCompilationAction(context => { AssertSame(context.Options); RegisterCompilationActionInvoked = true; });
4499+
context.RegisterOperationAction(context => { AssertSame(context.Options); RegisterOperationActionInvoked = true; }, OperationKind.Block);
4500+
context.RegisterOperationBlockAction(context => { AssertSame(context.Options); RegisterOperationBlockActionInvoked = true; });
4501+
context.RegisterSemanticModelAction(context => { AssertSame(context.Options); RegisterSemanticModelActionInvoked = true; });
4502+
context.RegisterSymbolAction(context => { AssertSame(context.Options); RegisterSymbolActionInvoked = true; }, SymbolKind.NamedType);
4503+
context.RegisterSyntaxNodeAction(context => { AssertSame(context.Options); RegisterSyntaxNodeActionInvoked = true; }, SyntaxKind.ClassDeclaration);
4504+
context.RegisterSyntaxTreeAction(context => { AssertSame(context.Options); RegisterSyntaxTreeActionInvoked = true; });
4505+
4506+
context.RegisterOperationBlockStartAction(context =>
4507+
{
4508+
AssertSame(context.Options);
4509+
RegisterOperationBlockStartActionInvoked = true;
4510+
context.RegisterOperationBlockEndAction(context =>
4511+
{
4512+
AssertSame(context.Options);
4513+
RegisterOperationBlockEndActionInvoked = true;
4514+
});
4515+
});
4516+
4517+
context.RegisterCompilationStartAction(context =>
4518+
{
4519+
AssertSame(context.Options);
4520+
RegisterCompilationStartActionInvoked = true;
4521+
context.RegisterCompilationEndAction(context =>
4522+
{
4523+
AssertSame(context.Options);
4524+
RegisterCompilationEndActionInvoked = true;
4525+
});
4526+
});
4527+
context.RegisterSymbolStartAction(context =>
4528+
{
4529+
AssertSame(context.Options);
4530+
RegisterSymbolStartActionInvoked = true;
4531+
context.RegisterSymbolEndAction(context =>
4532+
{
4533+
AssertSame(context.Options);
4534+
RegisterSymbolEndActionInvoked = true;
4535+
});
4536+
}, SymbolKind.NamedType);
4537+
}
4538+
4539+
public void AssertAllCallbacksInvoked()
4540+
{
4541+
Assert.NotNull(SeenOptions);
4542+
4543+
Assert.True(RegisterAdditionalFileActionInvoked);
4544+
4545+
Assert.True(RegisterAdditionalFileActionInvoked);
4546+
Assert.True(RegisterCodeBlockActionInvoked);
4547+
Assert.True(RegisterCodeBlockStartActionInvoked);
4548+
Assert.True(RegisterCompilationActionInvoked);
4549+
Assert.True(RegisterOperationActionInvoked);
4550+
Assert.True(RegisterOperationBlockActionInvoked);
4551+
Assert.True(RegisterSemanticModelActionInvoked);
4552+
Assert.True(RegisterSymbolActionInvoked);
4553+
Assert.True(RegisterSyntaxNodeActionInvoked);
4554+
Assert.True(RegisterSyntaxTreeActionInvoked);
4555+
4556+
Assert.True(RegisterOperationBlockStartActionInvoked);
4557+
Assert.True(RegisterOperationBlockEndActionInvoked);
4558+
Assert.True(RegisterCompilationStartActionInvoked);
4559+
Assert.True(RegisterCompilationEndActionInvoked);
4560+
Assert.True(RegisterSymbolStartActionInvoked);
4561+
Assert.True(RegisterSymbolEndActionInvoked);
4562+
}
4563+
}
4564+
4565+
[Fact]
4566+
public async Task TestAnalyzerSpecificOptionsFactory()
4567+
{
4568+
// lang=C#-Test
4569+
string source = """
4570+
class C
4571+
{
4572+
void M()
4573+
{
4574+
int x = 0;
4575+
}
4576+
}
4577+
""";
4578+
4579+
var tree = CSharpSyntaxTree.ParseText(source);
4580+
var compilation = CreateCompilationWithCSharp(new[] { tree, CSharpSyntaxTree.ParseText(IsExternalInitTypeDefinition) });
4581+
compilation.VerifyDiagnostics(
4582+
// (5,13): warning CS0219: The variable 'x' is assigned but its value is never used
4583+
// int x = 0;
4584+
Diagnostic(ErrorCode.WRN_UnreferencedVarAssg, "x").WithArguments("x").WithLocation(5, 13));
4585+
4586+
var additionalText = new InMemoryAdditionalText("path", "content");
4587+
4588+
// Ensure that the analyzer only sees the custom options passed to the callbacks, and never the shared options.
4589+
var sharedOptions = new AnalyzerOptions([additionalText]);
4590+
4591+
// Test1. Just a single analyzer. Ensure all callbacks get the custom options.
4592+
{
4593+
var customOptions = new CompilerAnalyzerConfigOptionsProvider(
4594+
ImmutableDictionary<object, AnalyzerConfigOptions>.Empty,
4595+
new DictionaryAnalyzerConfigOptions(
4596+
ImmutableDictionary<string, string>.Empty));
4597+
Assert.NotSame(sharedOptions, customOptions);
4598+
4599+
var analyzer = new OptionsOverrideDiagnosticAnalyzer(customOptions);
4600+
4601+
var compWithAnalyzers = new CompilationWithAnalyzers(
4602+
compilation,
4603+
[analyzer],
4604+
new CompilationWithAnalyzersOptions(
4605+
sharedOptions, onAnalyzerException: null, concurrentAnalysis: false, logAnalyzerExecutionTime: false, reportSuppressedDiagnostics: false, analyzerExceptionFilter: null,
4606+
_ => customOptions));
4607+
4608+
var diagnostics = await compWithAnalyzers.GetAllDiagnosticsAsync();
4609+
Assert.Single(diagnostics);
4610+
4611+
analyzer.AssertAllCallbacksInvoked();
4612+
}
4613+
4614+
// Test2. Two analyzers. Ensure both gets the custom options across all callbacks.
4615+
// Also, ensure that across the analyzers we're getting the exact same AnalyzerOptions instance.
4616+
{
4617+
var customOptions = new CompilerAnalyzerConfigOptionsProvider(
4618+
ImmutableDictionary<object, AnalyzerConfigOptions>.Empty,
4619+
new DictionaryAnalyzerConfigOptions(
4620+
ImmutableDictionary<string, string>.Empty));
4621+
Assert.NotSame(sharedOptions, customOptions);
4622+
4623+
var analyzer1 = new OptionsOverrideDiagnosticAnalyzer(customOptions);
4624+
var analyzer2 = new OptionsOverrideDiagnosticAnalyzer(customOptions);
4625+
4626+
var compWithAnalyzers = new CompilationWithAnalyzers(
4627+
compilation,
4628+
[analyzer1, analyzer2],
4629+
new CompilationWithAnalyzersOptions(
4630+
sharedOptions, onAnalyzerException: null, concurrentAnalysis: false, logAnalyzerExecutionTime: false, reportSuppressedDiagnostics: false, analyzerExceptionFilter: null,
4631+
_ => customOptions));
4632+
4633+
var diagnostics = await compWithAnalyzers.GetAllDiagnosticsAsync();
4634+
Assert.Single(diagnostics);
4635+
4636+
analyzer1.AssertAllCallbacksInvoked();
4637+
analyzer2.AssertAllCallbacksInvoked();
4638+
4639+
// Both analyzers should get the exact same AnalyzerOptions instance since they used the same customOptions.
4640+
Assert.Same(analyzer1.SeenOptions, analyzer2.SeenOptions);
4641+
}
4642+
}
44444643
}
44454644
}

src/Compilers/Core/CodeAnalysisTest/Diagnostics/DiagnosticLocalizationTests.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,21 @@ private static void TestDescriptorIsExceptionSafeCore(DiagnosticDescriptor descr
304304
Action<Exception, DiagnosticAnalyzer, Diagnostic, CancellationToken> onAnalyzerException = (ex, a, diag, ct) => exceptionDiagnostics.Add(diag);
305305
var analyzerManager = new AnalyzerManager(analyzer);
306306
var compilation = CSharp.CSharpCompilation.Create("test");
307-
var analyzerExecutor = AnalyzerExecutor.Create(compilation, AnalyzerOptions.Empty,
308-
addNonCategorizedDiagnostic: (_, _) => { }, onAnalyzerException, analyzerExceptionFilter: null,
309-
isCompilerAnalyzer: _ => false, analyzerManager, shouldSkipAnalysisOnGeneratedCode: _ => false,
310-
shouldSuppressGeneratedCodeDiagnostic: (_, _, _, _) => false, isGeneratedCodeLocation: (_, _, _) => false,
311-
isAnalyzerSuppressedForTree: (_, _, _, _) => false, getAnalyzerGate: _ => null,
307+
var analyzerExecutor = AnalyzerExecutor.Create(
308+
compilation,
309+
AnalyzerOptions.Empty,
310+
addNonCategorizedDiagnostic: (_, _, _) => { },
311+
onAnalyzerException,
312+
analyzerExceptionFilter: null,
313+
isCompilerAnalyzer: _ => false,
314+
diagnosticAnalyzers: [analyzer],
315+
getAnalyzerConfigOptionsProvider: null,
316+
analyzerManager,
317+
shouldSkipAnalysisOnGeneratedCode: _ => false,
318+
shouldSuppressGeneratedCodeDiagnostic: (_, _, _, _) => false,
319+
isGeneratedCodeLocation: (_, _, _) => false,
320+
isAnalyzerSuppressedForTree: (_, _, _, _) => false,
321+
getAnalyzerGate: _ => null,
312322
getSemanticModel: tree => compilation.GetSemanticModel(tree, ignoreAccessibility: true),
313323
SeverityFilter.None);
314324
var descriptors = analyzerManager.GetSupportedDiagnosticDescriptors(analyzer, analyzerExecutor, CancellationToken.None);

src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisScope.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,20 +95,19 @@ internal class AnalysisScope
9595

9696
public static AnalysisScope Create(Compilation compilation, ImmutableArray<DiagnosticAnalyzer> analyzers, CompilationWithAnalyzers compilationWithAnalyzers)
9797
{
98-
var analyzerOptions = compilationWithAnalyzers.AnalysisOptions.Options;
98+
var additionalFiles = compilationWithAnalyzers.AnalysisOptions.Options.GetAdditionalFiles();
9999
var hasAllAnalyzers = ComputeHasAllAnalyzers(analyzers, compilationWithAnalyzers);
100100
var concurrentAnalysis = compilationWithAnalyzers.AnalysisOptions.ConcurrentAnalysis;
101-
return Create(compilation, analyzerOptions, analyzers, hasAllAnalyzers, concurrentAnalysis);
101+
return Create(compilation, additionalFiles, analyzers, hasAllAnalyzers, concurrentAnalysis);
102102
}
103103

104-
public static AnalysisScope CreateForBatchCompile(Compilation compilation, AnalyzerOptions analyzerOptions, ImmutableArray<DiagnosticAnalyzer> analyzers)
104+
public static AnalysisScope CreateForBatchCompile(Compilation compilation, ImmutableArray<AdditionalText> additionalFiles, ImmutableArray<DiagnosticAnalyzer> analyzers)
105105
{
106-
return Create(compilation, analyzerOptions, analyzers, hasAllAnalyzers: true, concurrentAnalysis: compilation.Options.ConcurrentBuild);
106+
return Create(compilation, additionalFiles, analyzers, hasAllAnalyzers: true, concurrentAnalysis: compilation.Options.ConcurrentBuild);
107107
}
108108

109-
private static AnalysisScope Create(Compilation compilation, AnalyzerOptions? analyzerOptions, ImmutableArray<DiagnosticAnalyzer> analyzers, bool hasAllAnalyzers, bool concurrentAnalysis)
109+
private static AnalysisScope Create(Compilation compilation, ImmutableArray<AdditionalText> additionalFiles, ImmutableArray<DiagnosticAnalyzer> analyzers, bool hasAllAnalyzers, bool concurrentAnalysis)
110110
{
111-
var additionalFiles = analyzerOptions?.AdditionalFiles ?? ImmutableArray<AdditionalText>.Empty;
112111
return new AnalysisScope(compilation.CommonSyntaxTrees, additionalFiles,
113112
analyzers, hasAllAnalyzers, filterFile: null, filterSpanOpt: null,
114113
originalFilterFile: null, originalFilterSpan: null, isSyntacticSingleFileAnalysis: false,

0 commit comments

Comments
 (0)