Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow code fixes for source generator diagnostics #67953

Merged
merged 1 commit into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion src/EditorFeatures/Test/CodeFixes/CodeFixServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
using Xunit.Sdk;

namespace Microsoft.CodeAnalysis.Editor.UnitTests.CodeFixes
{
Expand Down Expand Up @@ -192,6 +193,40 @@ public async Task TestGetFixesAsyncForDocumentDiagnosticAnalyzerAsync()
Assert.True(documentDiagnosticAnalyzer.ReceivedCallback);
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/67354")]
public async Task TestGetFixesAsyncForGeneratorDiagnosticAsync()
{
// We have a special GeneratorDiagnosticsPlaceholderAnalyzer that report 0 SupportedDiagnostics.
// We need to ensure that we don't skip this special analyzer
// when computing the diagnostics/code fixes for "Normal" priority bucket, which
// normally only execute those analyzers which report at least one fixable supported diagnostic.
// Note that this special placeholder analyzer instance is always included for the project,
// we do not need to include it in the passed in analyzers.
Assert.Empty(GeneratorDiagnosticsPlaceholderAnalyzer.Instance.SupportedDiagnostics);

var analyzers = ImmutableArray<DiagnosticAnalyzer>.Empty;
var generator = new MockAnalyzerReference.MockGenerator();
var generators = ImmutableArray.Create<ISourceGenerator>(generator);
var fixTitle = "Fix Title";
var codeFix = new MockFixer(fixTitle);
var codeFixes = ImmutableArray.Create<CodeFixProvider>(codeFix);
var analyzerReference = new MockAnalyzerReference(codeFixes, analyzers, generators);

var tuple = ServiceSetup(codeFix, includeConfigurationFixProviders: false);
using var workspace = tuple.workspace;
GetDocumentAndExtensionManager(tuple.analyzerService, workspace, out var document, out var extensionManager, analyzerReference);

Assert.False(codeFix.Called);
var fixCollectionSet = await tuple.codeFixService.GetFixesAsync(document, TextSpan.FromBounds(0, 0),
priorityProvider: new DefaultCodeActionRequestPriorityProvider(CodeActionRequestPriority.Normal), CodeActionOptions.DefaultProvider,
addOperationScope: _ => null, cancellationToken: CancellationToken.None);
Assert.True(codeFix.Called);
var fixCollection = Assert.Single(fixCollectionSet);
Assert.Equal(MockFixer.Id, fixCollection.FirstDiagnostic.Id);
var fix = Assert.Single(fixCollection.Fixes);
Assert.Equal(fixTitle, fix.Action.Title);
}

[Fact]
public async Task TestGetCodeFixWithExceptionInRegisterMethod_Diagnostic()
{
Expand Down Expand Up @@ -395,9 +430,15 @@ private static IEnumerable<Lazy<CodeFixProvider, CodeChangeProviderMetadata>> Cr
internal class MockFixer : CodeFixProvider
{
public const string Id = "MyDiagnostic";
private readonly string? _registerFixWithTitle;
public bool Called;
public int ContextDiagnosticsCount;

public MockFixer(string? registerFixWithTitle = null)
{
_registerFixWithTitle = registerFixWithTitle;
}

public sealed override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(Id); }
Expand All @@ -407,6 +448,15 @@ public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
{
Called = true;
ContextDiagnosticsCount = context.Diagnostics.Length;
if (_registerFixWithTitle != null)
{
context.RegisterCodeFix(
CodeAction.Create(
_registerFixWithTitle,
createChangedDocument: _ => Task.FromResult(context.Document)),
context.Diagnostics);
}

return Task.CompletedTask;
}
}
Expand All @@ -415,14 +465,21 @@ private class MockAnalyzerReference : AnalyzerReference, ICodeFixProviderFactory
{
public readonly ImmutableArray<CodeFixProvider> Fixers;
public readonly ImmutableArray<DiagnosticAnalyzer> Analyzers;
public readonly ImmutableArray<ISourceGenerator> Generators;

private static readonly ImmutableArray<CodeFixProvider> s_defaultFixers = ImmutableArray.Create<CodeFixProvider>(new MockFixer());
private static readonly ImmutableArray<DiagnosticAnalyzer> s_defaultAnalyzers = ImmutableArray.Create<DiagnosticAnalyzer>(new MockDiagnosticAnalyzer());

public MockAnalyzerReference(ImmutableArray<CodeFixProvider> fixers, ImmutableArray<DiagnosticAnalyzer> analyzers)
public MockAnalyzerReference(ImmutableArray<CodeFixProvider> fixers, ImmutableArray<DiagnosticAnalyzer> analyzers, ImmutableArray<ISourceGenerator> generators)
{
Fixers = fixers;
Analyzers = analyzers;
Generators = generators;
}

public MockAnalyzerReference(ImmutableArray<CodeFixProvider> fixers, ImmutableArray<DiagnosticAnalyzer> analyzers)
: this(fixers, analyzers, ImmutableArray<ISourceGenerator>.Empty)
{
}

public MockAnalyzerReference(CodeFixProvider? fixer, ImmutableArray<DiagnosticAnalyzer> analyzers)
Expand Down Expand Up @@ -471,6 +528,9 @@ public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzers(string language)
public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzersForAllLanguages()
=> ImmutableArray<DiagnosticAnalyzer>.Empty;

public override ImmutableArray<ISourceGenerator> GetGenerators(string language)
=> Generators;

public ImmutableArray<CodeFixProvider> GetFixers()
=> Fixers;

Expand Down Expand Up @@ -555,6 +615,23 @@ public override Task<ImmutableArray<Diagnostic>> AnalyzeSemanticsAsync(Document
return Task.FromResult(ImmutableArray<Diagnostic>.Empty);
}
}

public class MockGenerator : ISourceGenerator
{
private readonly DiagnosticDescriptor s_descriptor = new(MockFixer.Id, "Title", "Message", "Category", DiagnosticSeverity.Warning, isEnabledByDefault: true);

public void Initialize(GeneratorInitializationContext context)
{
}

public void Execute(GeneratorExecutionContext context)
{
foreach (var tree in context.Compilation.SyntaxTrees)
{
context.ReportDiagnostic(Diagnostic.Create(s_descriptor, tree.GetLocation(new TextSpan(0, 1))));
}
}
}
}

internal class TestErrorLogger : IErrorLoggerService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,13 @@ static bool ShouldIncludeAnalyzer(
if (analyzer is DocumentDiagnosticAnalyzer)
return true;

// Special case GeneratorDiagnosticsPlaceholderAnalyzer to never skip it based on
// 'shouldIncludeDiagnostic' predicate. More specifically, this is a placeholder analyzer
// for threading through all source generator reported diagnostics, but this special analyzer
// reports 0 supported diagnostics, and we always want to execute it.
if (analyzer is GeneratorDiagnosticsPlaceholderAnalyzer)
return true;

// Skip analyzer if none of its reported diagnostics should be included.
if (shouldIncludeDiagnostic != null &&
!owner.DiagnosticAnalyzerInfoCache.GetDiagnosticDescriptors(analyzer).Any(static (a, shouldIncludeDiagnostic) => shouldIncludeDiagnostic(a.Id), shouldIncludeDiagnostic))
Expand Down