Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -2487,6 +2487,9 @@ public override void Initialize(AnalysisContext context)
}
}

/// <summary>
/// An analyzer that reports a diagnostic on every single named type.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class NamedTypeAnalyzerWithConfigurableEnabledByDefault : DiagnosticAnalyzer
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
Expand Down Expand Up @@ -652,6 +653,166 @@ internal async Task TestOnlyRequiredAnalyzerExecutedDuringDiagnosticComputation(
Assert.Empty(diagnosticMap.Other);
}

[Theory, CombinatorialData]
[WorkItem("https://github.com/dotnet/roslyn/issues/79706")]
internal async Task TestAnalysisWithAnalyzerInBothProjectAndHost_SameAnalyzerInstance(bool documentAnalysis)
{
using var workspace = TestWorkspace.CreateCSharp("class A { }");

var analyzer = new NamedTypeAnalyzerWithConfigurableEnabledByDefault(isEnabledByDefault: true, DiagnosticSeverity.Warning, throwOnAllNamedTypes: false);
var analyzerId = analyzer.GetAnalyzerId();
var analyzerReference = new AnalyzerImageReference([analyzer]);

workspace.TryApplyChanges(
workspace.CurrentSolution.WithAnalyzerReferences([analyzerReference])
.Projects.Single().AddAnalyzerReference(analyzerReference).Solution);

var project = workspace.CurrentSolution.Projects.Single();
var document = documentAnalysis ? project.Documents.Single() : null;
var diagnosticsMapResults = await DiagnosticComputer.GetDiagnosticsAsync(
document, project, Checksum.Null, span: null, projectAnalyzerIds: [analyzerId], [analyzerId],
AnalysisKind.Semantic, new DiagnosticAnalyzerInfoCache(), workspace.Services,
logPerformanceInfo: false, getTelemetryInfo: false,
cancellationToken: CancellationToken.None);

// In this case, since the analyzer identity is identical, we ran it once
var analyzerResults = diagnosticsMapResults.Diagnostics.Single();
Assert.Equal(analyzerId, analyzerResults.Item1);
Assert.Equal(1, analyzerResults.Item2.Semantic.Length);
}

[Theory, CombinatorialData]
[WorkItem("https://github.com/dotnet/roslyn/issues/79706")]
internal async Task TestAnalysisWithAnalyzerInBothProjectAndHost_DifferentAnalyzerInstancesFromNonEqualReferences(bool documentAnalysis)
{
using var workspace = TestWorkspace.CreateCSharp("class A { }");

var analyzerProject = new NamedTypeAnalyzerWithConfigurableEnabledByDefault(isEnabledByDefault: true, DiagnosticSeverity.Warning, throwOnAllNamedTypes: false);
var analyzerProjectId = analyzerProject.GetAnalyzerId();
var analyzerProjectReference = new AnalyzerImageReference([analyzerProject]);

var analyzerHost = new NamedTypeAnalyzerWithConfigurableEnabledByDefault(isEnabledByDefault: true, DiagnosticSeverity.Warning, throwOnAllNamedTypes: false);
var analyzerHostId = analyzerHost.GetAnalyzerId();
var analyzerHostReference = new AnalyzerImageReference([analyzerHost]);

// AnalyzerImageReference will create a separate AnalyzerImageReference.Id for each instance created, so these will be different.
Assert.NotEqual(analyzerProjectReference.Id, analyzerHostReference.Id);
Assert.Equal(analyzerProjectId, analyzerHostId);

workspace.TryApplyChanges(
workspace.CurrentSolution.WithAnalyzerReferences([analyzerHostReference])
.Projects.Single().AddAnalyzerReference(analyzerProjectReference).Solution);

var project = workspace.CurrentSolution.Projects.Single();
var document = documentAnalysis ? project.Documents.Single() : null;
var diagnosticsMapResults = await DiagnosticComputer.GetDiagnosticsAsync(
document, project, Checksum.Null, span: null, projectAnalyzerIds: [analyzerProjectId], [analyzerHostId],
AnalysisKind.Semantic, new DiagnosticAnalyzerInfoCache(), workspace.Services,
logPerformanceInfo: false, getTelemetryInfo: false,
cancellationToken: CancellationToken.None);

// In this case, since the analyzer reference identity is identical, we ran it once
var analyzerResults = diagnosticsMapResults.Diagnostics.Single();
Assert.Equal(analyzerHostId, analyzerResults.Item1);
Assert.Equal(1, analyzerResults.Item2.Semantic.Length);
}

[Theory, CombinatorialData]
[WorkItem("https://github.com/dotnet/roslyn/issues/79706")]
internal async Task TestAnalysisWithAnalyzerInBothProjectAndHost_DifferentAnalyzerInstancesFromEqualReferences(bool documentAnalysis, bool includeExtraProjectReference, bool includeExtraHostReference)
{
using var workspace = TestWorkspace.CreateCSharp("class A { }");

var analyzerProject = new NamedTypeAnalyzerWithConfigurableEnabledByDefault(isEnabledByDefault: true, DiagnosticSeverity.Warning, throwOnAllNamedTypes: false);
var analyzerProjectId = analyzerProject.GetAnalyzerId();
var analyzerProjectReference = CreateAnalyzerReferenceWithSameId(analyzerProject);

var analyzerHost = new NamedTypeAnalyzerWithConfigurableEnabledByDefault(isEnabledByDefault: true, DiagnosticSeverity.Warning, throwOnAllNamedTypes: false);
var analyzerHostId = analyzerHost.GetAnalyzerId();
var analyzerHostReference = CreateAnalyzerReferenceWithSameId(analyzerHost);

Assert.Equal(analyzerProjectReference.Id, analyzerHostReference.Id);
Assert.Equal(analyzerProjectId, analyzerHostId);

workspace.TryApplyChanges(
workspace.CurrentSolution.WithAnalyzerReferences(AddExtraReferenceIfNeeded(analyzerHostReference, includeExtraHostReference))
.Projects.Single().AddAnalyzerReferences(AddExtraReferenceIfNeeded(analyzerProjectReference, includeExtraProjectReference)).Solution);

var project = workspace.CurrentSolution.Projects.Single();
var document = documentAnalysis ? project.Documents.Single() : null;
var diagnosticsMapResults = await DiagnosticComputer.GetDiagnosticsAsync(
document, project, Checksum.Null, span: null, projectAnalyzerIds: [analyzerProjectId], [analyzerHostId],
AnalysisKind.Semantic, new DiagnosticAnalyzerInfoCache(), workspace.Services,
logPerformanceInfo: false, getTelemetryInfo: false,
cancellationToken: CancellationToken.None);

// In this case, the analyzers are ran twice. This appears to be a bug in SkippedHostAnalyzersInfo.Create, because it calls
// HostDiagnosticAnalyzers.CreateProjectDiagnosticAnalyzersPerReference which already filters out references, it doesn't return any
// references to skip.
Comment on lines +749 to +751
Copy link
Member Author

@jasonmalinowski jasonmalinowski Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is entirely possible this is the "real" bug in this scenario. Fundamentally, the issue happening on user machines here is we have host-provided analyzers and project-provided analyzers that are overlapping in various ways, which eventually leads to crashes in the DiagnosticComputer. You can imagine fixing this two ways:

  1. We fix the logic for deduplication so that DiagnosticComputer never gets duplicates in any way.
  2. We fix DiagnosticComputer to deal with the duplicates.

I'm doing 2 in this PR. That's the approach I was following until writing this test, and then I had to debug what's going on here, so at this point I'm biased towards that! But for 17.14, I suspect that's the right thing to do, since:

  1. DiagnosticComputer has comments and code trying to deal with the duplicates (which makes me think it is expected to treat that as valid input.)
  2. The changes there to fix the duplicate handling appear to be straightforward changes to the code and addressing simple oversights, which is higher confidence than trying to ensure it never gets invalid inputs in the first place.

Assert.Equal(2, diagnosticsMapResults.Diagnostics.Length);

static AnalyzerReference CreateAnalyzerReferenceWithSameId(DiagnosticAnalyzer analyzer)
{
var map = new Dictionary<string, ImmutableArray<DiagnosticAnalyzer>>()
{
{ LanguageNames.CSharp, [analyzer] }
};

return new TestAnalyzerReferenceByLanguage(map);
}

static ImmutableArray<AnalyzerReference> AddExtraReferenceIfNeeded(AnalyzerReference mainReference, bool addExtraReference)
{
if (addExtraReference)
{
return [mainReference, new AnalyzerImageReference([new FieldAnalyzer("FA1234", syntaxTreeAction: false)])];
}
else
{
return [mainReference];
}
}
}

[Theory, CombinatorialData]
[WorkItem("https://github.com/dotnet/roslyn/issues/79706")]
internal async Task TestAnalysisWithAnalyzerInBothProjectAndHost_SameAnalyzerInstancesFromEqualReferences(bool documentAnalysis)
{
using var workspace = TestWorkspace.CreateCSharp("class A { }");

var analyzer = new NamedTypeAnalyzerWithConfigurableEnabledByDefault(isEnabledByDefault: true, DiagnosticSeverity.Warning, throwOnAllNamedTypes: false);
var analyzerProjectReference = CreateAnalyzerReferenceWithSameId(analyzer);
var analyzerHostReference = CreateAnalyzerReferenceWithSameId(analyzer);

var analyzerId = analyzer.GetAnalyzerId();

Assert.Equal(analyzerProjectReference.Id, analyzerHostReference.Id);

workspace.TryApplyChanges(
workspace.CurrentSolution.WithAnalyzerReferences([analyzerHostReference])
.Projects.Single().AddAnalyzerReference(analyzerProjectReference).Solution);

var project = workspace.CurrentSolution.Projects.Single();
var document = documentAnalysis ? project.Documents.Single() : null;
var diagnosticsMapResults = await DiagnosticComputer.GetDiagnosticsAsync(
document, project, Checksum.Null, span: null, projectAnalyzerIds: [analyzerId], [analyzerId],
AnalysisKind.Semantic, new DiagnosticAnalyzerInfoCache(), workspace.Services,
logPerformanceInfo: false, getTelemetryInfo: false,
cancellationToken: CancellationToken.None);

Assert.Equal(1, diagnosticsMapResults.Diagnostics.Length);

static AnalyzerReference CreateAnalyzerReferenceWithSameId(DiagnosticAnalyzer analyzer)
{
var map = new Dictionary<string, ImmutableArray<DiagnosticAnalyzer>>()
{
{ LanguageNames.CSharp, [analyzer] }
};

return new TestAnalyzerReferenceByLanguage(map);
}
}

[Theory, WorkItem(67257, "https://github.com/dotnet/roslyn/issues/67257")]
[CombinatorialData]
public async Task TestFilterSpanOnContextAsync(FilterSpanTestAnalyzer.AnalysisKind kind)
Expand Down
7 changes: 7 additions & 0 deletions src/Workspaces/Core/Portable/Diagnostics/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ public static async Task<ImmutableDictionary<DiagnosticAnalyzer, DiagnosticAnaly
var builder = ImmutableDictionary.CreateBuilder<DiagnosticAnalyzer, DiagnosticAnalysisResultBuilder>();
foreach (var analyzer in projectAnalyzers.ConcatFast(hostAnalyzers))
{
if (builder.ContainsKey(analyzer))
{
// If we already have a result for this analyzer, we had a duplicate. We already processed the results
// for this so no reason to process it a second time.
continue;
}

cancellationToken.ThrowIfCancellationRequested();

if (skippedAnalyzersInfo.SkippedAnalyzers.Contains(analyzer))
Expand Down
Loading