diff --git a/src/FakeAnalyzers/DiagnosticDescriptors.cs b/src/FakeAnalyzers/DiagnosticDescriptors.cs index f6dea02..5263669 100644 --- a/src/FakeAnalyzers/DiagnosticDescriptors.cs +++ b/src/FakeAnalyzers/DiagnosticDescriptors.cs @@ -27,4 +27,12 @@ public static class DiagnosticDescriptors category: "Code", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor TestFlagEnabled = new( + id: DiagnosticIds.TestFlagEnabled, + title: "Test flag is enabled", + messageFormat: "The test flag is enabled for '{0}'", + category: "Code", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); } \ No newline at end of file diff --git a/src/FakeAnalyzers/DiagnosticIds.cs b/src/FakeAnalyzers/DiagnosticIds.cs index 83a7fd0..fb06723 100644 --- a/src/FakeAnalyzers/DiagnosticIds.cs +++ b/src/FakeAnalyzers/DiagnosticIds.cs @@ -9,4 +9,5 @@ public static class DiagnosticIds public const string NowUsedInsteadOfUtcNow = "FAKE0001"; public const string AsyncVoid = "FAKE0002"; public const string IdentifierContainsFoo = "FAKE0003"; + public const string TestFlagEnabled = "FAKE0004"; } \ No newline at end of file diff --git a/src/FakeAnalyzers/TestFlagAnalyzer.cs b/src/FakeAnalyzers/TestFlagAnalyzer.cs new file mode 100644 index 0000000..0f2dfe5 --- /dev/null +++ b/src/FakeAnalyzers/TestFlagAnalyzer.cs @@ -0,0 +1,40 @@ +namespace FakeAnalyzers; + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class TestFlagAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => [DiagnosticDescriptors.TestFlagEnabled]; + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(startContext => + { + if (!startContext.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.TestFlag", out var value) || value != "enabled") + { + return; + } + + startContext.RegisterSyntaxNodeAction(AnalyzeClass, SyntaxKind.ClassDeclaration); + }); + } + + static void AnalyzeClass(SyntaxNodeAnalysisContext context) + { + if (context.Node is not ClassDeclarationSyntax classDeclaration) + { + return; + } + + var diagnostic = Diagnostic.Create(DiagnosticDescriptors.TestFlagEnabled, classDeclaration.Identifier.GetLocation(), classDeclaration.Identifier.Text); + context.ReportDiagnostic(diagnostic); + } +} \ No newline at end of file diff --git a/src/Particular.AnalyzerTesting/AnalyzerConfigOptionsFactory.cs b/src/Particular.AnalyzerTesting/AnalyzerConfigOptionsFactory.cs new file mode 100644 index 0000000..f9ed5d2 --- /dev/null +++ b/src/Particular.AnalyzerTesting/AnalyzerConfigOptionsFactory.cs @@ -0,0 +1,27 @@ +namespace Particular.AnalyzerTesting; + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +static class AnalyzerConfigOptionsFactory +{ + public static AnalyzerConfigOptionsProvider CreateOptionsProvider(IReadOnlyDictionary properties) + => new OptionsProvider(new DictionaryAnalyzerConfigOptions(properties)); + + public static AnalyzerOptions CreateAnalyzerOptions(IReadOnlyDictionary properties) + => new([], CreateOptionsProvider(properties)); + + sealed class OptionsProvider(AnalyzerConfigOptions options) : AnalyzerConfigOptionsProvider + { + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => options; + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => options; + public override AnalyzerConfigOptions GlobalOptions => options; + } + + sealed class DictionaryAnalyzerConfigOptions(IReadOnlyDictionary properties) : AnalyzerConfigOptions + { + public override bool TryGetValue(string key, out string value) + => properties.TryGetValue(key, out value!); + } +} \ No newline at end of file diff --git a/src/Particular.AnalyzerTesting/BaseAnalyzerTest.cs b/src/Particular.AnalyzerTesting/BaseAnalyzerTest.cs index aeadfb8..15f96d2 100644 --- a/src/Particular.AnalyzerTesting/BaseAnalyzerTest.cs +++ b/src/Particular.AnalyzerTesting/BaseAnalyzerTest.cs @@ -86,7 +86,7 @@ private protected static async Task GetCompilerDiagnostics(Project private protected async Task GetAnalyzerDiagnostics(Compilation compilation, string[] ignoreDiagnosticIds, CancellationToken cancellationToken = default) { var analyzerTasks = analyzers - .Select(analyzer => compilation.GetAnalyzerDiagnostics(analyzer, cancellationToken)) + .Select(analyzer => compilation.GetAnalyzerDiagnostics(analyzer, features, cancellationToken)) .ToArray(); await Task.WhenAll(analyzerTasks); @@ -212,4 +212,4 @@ static void OutputAnalyzerDiagnostics(Diagnostic[] analyzerDiagnostics) private protected record SourceFile(string Filename, string Source, TextSpan[] Spans); private protected record DiagnosticInfo(string Filename, TextSpan Span, string Id); -} \ No newline at end of file +} diff --git a/src/Particular.AnalyzerTesting/CompilationExtensions.cs b/src/Particular.AnalyzerTesting/CompilationExtensions.cs index f2848f6..e63e46c 100644 --- a/src/Particular.AnalyzerTesting/CompilationExtensions.cs +++ b/src/Particular.AnalyzerTesting/CompilationExtensions.cs @@ -32,12 +32,12 @@ public void Compile(bool throwOnFailure = true) Debug.WriteLine("Compilation failed."); } - public async Task> GetAnalyzerDiagnostics(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken = default) + public async Task> GetAnalyzerDiagnostics(DiagnosticAnalyzer analyzer, IReadOnlyDictionary properties, CancellationToken cancellationToken = default) { var exceptions = new List(); var analysisOptions = new CompilationWithAnalyzersOptions( - new AnalyzerOptions([]), + AnalyzerConfigOptionsFactory.CreateAnalyzerOptions(properties), (exception, _, __) => exceptions.Add(exception), concurrentAnalysis: false, logAnalyzerExecutionTime: false); @@ -56,4 +56,4 @@ public async Task> GetAnalyzerDiagnostics(DiagnosticAnal .ThenBy(diagnostic => diagnostic.Id); } } -} \ No newline at end of file +} diff --git a/src/Particular.AnalyzerTesting/SourceGeneratorBuild.cs b/src/Particular.AnalyzerTesting/SourceGeneratorBuild.cs index b13785f..629afb6 100644 --- a/src/Particular.AnalyzerTesting/SourceGeneratorBuild.cs +++ b/src/Particular.AnalyzerTesting/SourceGeneratorBuild.cs @@ -11,18 +11,25 @@ class SourceGeneratorBuild readonly GeneratorDriver driver; readonly ImmutableArray analyzers; readonly ImmutableArray suppressors; + readonly AnalyzerConfigOptionsProvider optionsProvider; - public SourceGeneratorBuild(Compilation initialCompilation, GeneratorDriver driver, ImmutableArray analyzers, ImmutableArray suppressors) + public SourceGeneratorBuild(Compilation initialCompilation, GeneratorDriver driver, ImmutableArray analyzers, ImmutableArray suppressors, AnalyzerConfigOptionsProvider optionsProvider) { this.initialCompilation = initialCompilation; this.driver = driver.RunGeneratorsAndUpdateCompilation(initialCompilation, out var outputCompilation, out var generatorDiagnostics); this.analyzers = analyzers; this.suppressors = suppressors; + this.optionsProvider = optionsProvider; RunResult = this.driver.GetRunResult(); var allAnalyzers = analyzers.Concat(suppressors).ToImmutableArray(); - OutputCompilation = outputCompilation.WithAnalyzers(allAnalyzers); + var analysisOptions = new CompilationWithAnalyzersOptions( + new([], optionsProvider), + onAnalyzerException: null, + concurrentAnalysis: false, + logAnalyzerExecutionTime: false); + OutputCompilation = outputCompilation.WithAnalyzers(allAnalyzers, analysisOptions); GeneratorDiagnostics = generatorDiagnostics; } @@ -33,6 +40,6 @@ public SourceGeneratorBuild(Compilation initialCompilation, GeneratorDriver driv public SourceGeneratorBuild Clone() { var cloneCompilation = initialCompilation.Clone(); - return new SourceGeneratorBuild(cloneCompilation, driver, analyzers, suppressors); + return new SourceGeneratorBuild(cloneCompilation, driver, analyzers, suppressors, optionsProvider); } } \ No newline at end of file diff --git a/src/Particular.AnalyzerTesting/SourceGeneratorTest.cs b/src/Particular.AnalyzerTesting/SourceGeneratorTest.cs index 72a5c31..47480d7 100644 --- a/src/Particular.AnalyzerTesting/SourceGeneratorTest.cs +++ b/src/Particular.AnalyzerTesting/SourceGeneratorTest.cs @@ -170,7 +170,7 @@ public SourceGeneratorTestResult Run() disabledOutputs: IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true); - var optsProvider = new OptionsProvider(new DictionaryAnalyzerOptions(features)); + var optsProvider = AnalyzerConfigOptionsFactory.CreateOptionsProvider(features); var driver = CSharpGeneratorDriver.Create(generators, driverOptions: driverOpts, @@ -183,7 +183,7 @@ public SourceGeneratorTestResult Run() ImmutableArray analyzersToUse = analyzers.Count > 0 ? [.. analyzers] : [new NoOpAnalyzer()]; ImmutableArray suppressorsToUse = suppressors.Count > 0 ? [.. suppressors] : []; - build = new SourceGeneratorBuild(initialCompilation, driver, analyzersToUse, suppressorsToUse); + build = new SourceGeneratorBuild(initialCompilation, driver, analyzersToUse, suppressorsToUse, optsProvider); try { @@ -261,21 +261,6 @@ static bool TryGetTrackingNames(Type generatorType, out IReadOnlyCollection options; - public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => options; - public override AnalyzerConfigOptions GlobalOptions => options; - } - - internal sealed class DictionaryAnalyzerOptions(Dictionary properties) : AnalyzerConfigOptions - { - public static DictionaryAnalyzerOptions Empty { get; } = new([]); - - public override bool TryGetValue(string key, out string value) - => properties.TryGetValue(key, out value!); - } } /// diff --git a/src/Tests/Analyzers/TestFlagAnalyzerTests.cs b/src/Tests/Analyzers/TestFlagAnalyzerTests.cs new file mode 100644 index 0000000..37880fe --- /dev/null +++ b/src/Tests/Analyzers/TestFlagAnalyzerTests.cs @@ -0,0 +1,22 @@ +namespace Tests; + +using System.Threading.Tasks; +using FakeAnalyzers; +using NUnit.Framework; +using Particular.AnalyzerTesting; + +public class TestFlagAnalyzerTests +{ + const string Code = """ + public class [|MyClass|] + { + } + """; + + [Test] + public Task ReportsDiagnosticWhenPropertyIsEnabled() => + AnalyzerTest.ForAnalyzer() + .WithProperty("build_property.TestFlag", "enabled") + .WithSource(Code) + .AssertDiagnostics(DiagnosticIds.TestFlagEnabled); +} \ No newline at end of file diff --git a/src/Tests/SourceGenerators/BasicSourceGeneratorTest.cs b/src/Tests/SourceGenerators/BasicSourceGeneratorTest.cs index 5d73576..a2b12ba 100644 --- a/src/Tests/SourceGenerators/BasicSourceGeneratorTest.cs +++ b/src/Tests/SourceGenerators/BasicSourceGeneratorTest.cs @@ -1,5 +1,6 @@ namespace Tests.SourceGenerators; +using System.Linq; using System.Threading.Tasks; using FakeAnalyzers; using NUnit.Framework; @@ -7,47 +8,60 @@ namespace Tests.SourceGenerators; public class BasicSourceGeneratorTest { + const string Source = $$""" + using System; + + [AttributeUsage(AttributeTargets.All)] + public class MarkerAttribute : Attribute { } + + [Marker] + public class Hello + { + [Marker] + private string there = "foo"; + + [Marker] + public DateTime Enjoy { get; set; } + + public void Use() + { + _ = the; + _ = there; + } + + [Marker] + private string the; + + public void DoArguments([Marker] string test, [Marker] Hello results) + { + the = test; + } + } + + """; + [Test] public async Task BasicTest() { - var source = $$""" - using System; - - [AttributeUsage(AttributeTargets.All)] - public class MarkerAttribute : Attribute { } - - [Marker] - public class Hello - { - [Marker] - private string there = "foo"; - - [Marker] - public DateTime Enjoy { get; set; } - - public void Use() - { - _ = the; - _ = there; - } - - [Marker] - private string the; - - public void DoArguments([Marker] string test, [Marker] Hello results) - { - the = test; - } - } - - """; - SourceGeneratorTest.ForIncrementalGenerator() - .WithSource(source) + .WithSource(Source) .Run() .Approve() .ToConsole() .AssertRunsAreEqual() .OutputSteps(); } -} \ No newline at end of file + + [Test] + public void AnalyzerSeesPropertyDuringSourceGeneratorRun() + { + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithAnalyzer() + .WithProperty("build_property.TestFlag", "enabled") + .SuppressCompilationErrors() + .WithSource(Source) + .Run(); + + Assert.That(result.AnalyzerDiagnostics.Select(diagnostic => diagnostic.Id), Contains.Item(DiagnosticIds.TestFlagEnabled)); + } +}