3

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.

LoadIt
  • 137
  • 1
  • 1
  • 9

1 Answers1

3

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.

FlashOver
  • 1,855
  • 1
  • 13
  • 24
  • The blogpost describes that using a .props file and including it in the nuget build makes it possible for the project that is using the analyzer to only add the property in a property group. But this is not working, I always have to include the CompilerVisibleProperty in the project using the analyzer aswell @FlashOver – d0neall Feb 02 '23 at 16:04
  • I figured it out. In my case, I had to add $(AssemblyName).props in the include string – d0neall Feb 02 '23 at 16:22