Search code examples
roslynroslyn-code-analysismicrosoft.codeanalysis

Roslyn CodeFix - MSBuild Properties/metadata and Unit testing


I'm building a roslyn analyzer/code fix but I wan't to access the MSBuild Properties and metadata (both from Directory.build.props and the .csproj) in order to know how to apply the code fix. I only found documentation to do it in source generators but not for analyzers.

To be more specific I want to know if the project is configured to use the new ImplicitUsings, but it would be usefull to also have access to everything.

Also do we have any way to get all the project global usings?

And using the new Microsoft.CodeAnalysis.Testing how can I add the MSBuild property so I can actually test it?

Regards.


Solution

  • Accessing MSBuild properties and metadata in DiagnosticAnalyzers is actually quite similar to how they're read and tested in ISourceGenerators/IIncrementalGenerators, since Source Generators are technically .NET analyzers as well.

    I assume that the documentation you've mentioned is the Source Generators Cookbook.

    First, we need to make the MSBuild property available to the global analyzer config options of the analyzer:

    <Project>
      <ItemGroup>
        <CompilerVisibleProperty Include="MyAnalyzer_MyProperty" />
      </ItemGroup>
    </Project>
    

    Then, we may read the value of that property from the AnalyzerConfigOptionsProvider's GlobalOptions. You'll find it within the parameter of the AnalysisContext.Register* method of your choice that you use within your overridden DiagnosticAnalyzer.Initialize(AnalysisContext) method. For example RegisterCompilationAction:

    bool isEnabled = false;
    if (compilationAnalysisContext.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.MyAnalyzer_MyProperty", out string? value))
    {
        isEnabled = value.Equals("true", StringComparison.OrdinalIgnoreCase) || value.Equals("enable", StringComparison.OrdinalIgnoreCase);
    }
    
    ImmutableDictionary<string, string?>.Builder properties = ImmutableDictionary.CreateBuilder<string, string?>();
    if (isEnabled)
    {
        properties.Add("IsEnabled", value);
    }
    
    var diagnostic = Diagnostic.Create(Rule, location, properties.ToImmutable());
    compilationAnalysisContext.ReportDiagnostic(diagnostic);
    

    The CodeFixProvider's CodeFixContext does not have a dedicated AnalyzerOptions Options property, but you may pass the value via the Diagnostic.Properties:

    foreach (Diagnostic diagnostic in context.Diagnostics)
    {
        if (diagnostic.Properties.TryGetValue("IsEnabled", out string? value))
        {
            var action = CodeAction.Create(Title, cancellationToken => OnCreateChangedDocument(context.Document, cancellationToken), diagnostic.Id);
            context.RegisterCodeFix(action, diagnostic);
        }
    }
    

    ... or, what I just discovered while composing this answer, access the AnalyzerConfigOptionsProvider through CodeFixContext.Document.Project.AnalyzerOptions. This works wherever you have a Document (or Project) available:

    public override Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        bool hasValue = context.Document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.MyAnalyzer_MyProperty", out string? value);
    }
    

    Additionally, it works with CodeRefactoringProvider:

    public override Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        bool hasValue = context.Document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.MyAnalyzer_MyProperty", out string? value);
    
        return Task.CompletedTask;
    }
    

    ... and with CompletionProvider:

    public override Task ProvideCompletionsAsync(CompletionContext context)
    {
        bool hasValue = context.Document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.MyAnalyzer_MyProperty", out string? value);
    
        return Task.CompletedTask;
    }
    

    ... and also with DiagnosticSuppressor:

    public override void ReportSuppressions(SuppressionAnalysisContext context)
    {
        bool hasValue = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.MyAnalyzer_MyProperty", out string? value);
    }
    

    And for testing via Microsoft.CodeAnalysis.Testing, you can add the global analyzer config option via ProjectState.AnalyzerConfigFiles of the AnalyzerTest<TVerifier>'s SolutionState TestState property:

    string config = $"is_global = true{Environment.NewLine}build_property.MyAnalyzer_MyProperty = {true}";
    analyzerTest.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", config));
    

    Above I described the usage with the custom MSBuild property MyAnalyzer_MyProperty, but of course it works with the well-known ImplicitUsings property too.