|
23 | 23 | using Microsoft.CodeAnalysis.Test.Utilities;
|
24 | 24 | using Microsoft.CodeAnalysis.Text;
|
25 | 25 | using Roslyn.Test.Utilities;
|
| 26 | +using Roslyn.Test.Utilities.TestGenerators; |
26 | 27 | using Roslyn.Utilities;
|
27 | 28 | using Xunit;
|
28 | 29 |
|
@@ -4441,5 +4442,203 @@ record B(int I) : A(I);
|
4441 | 4442 | var diagnostics = await compWithAnalyzers.GetAnalyzerSemanticDiagnosticsAsync(model, filterSpan: null, CancellationToken.None);
|
4442 | 4443 | diagnostics.Verify(Diagnostic("ID0001", "B").WithLocation(1, 8));
|
4443 | 4444 | }
|
| 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 | + } |
4444 | 4643 | }
|
4445 | 4644 | }
|
0 commit comments