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

Cache diagnostic info based on project state not project. #77230

Merged
merged 13 commits into from
Feb 14, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,13 @@ private partial class DiagnosticIncrementalAnalyzer
private partial class StateManager
{
private HostAnalyzerInfo GetOrCreateHostAnalyzerInfo(
Project project, ProjectAnalyzerInfo projectAnalyzerInfo)
SolutionState solution, ProjectState project, ProjectAnalyzerInfo projectAnalyzerInfo)
{
var analyzers = project.Solution.SolutionState.Analyzers;
var key = new HostAnalyzerInfoKey(project.Language, project.State.HasSdkCodeStyleAnalyzers, analyzers.HostAnalyzerReferences);
var key = new HostAnalyzerInfoKey(project.Language, project.HasSdkCodeStyleAnalyzers, solution.Analyzers.HostAnalyzerReferences);
// Some Host Analyzers may need to be treated as Project Analyzers so that they do not have access to the
// Host fallback options. These ids will be used when building up the Host and Project analyzer collections.
var referenceIdsToRedirect = GetReferenceIdsToRedirectAsProjectAnalyzers(project);
var hostAnalyzerInfo = ImmutableInterlocked.GetOrAdd(ref _hostAnalyzerStateMap, key, CreateLanguageSpecificAnalyzerMap, (Analyzers: analyzers, referenceIdsToRedirect));
var referenceIdsToRedirect = GetReferenceIdsToRedirectAsProjectAnalyzers(solution, project);
var hostAnalyzerInfo = ImmutableInterlocked.GetOrAdd(ref _hostAnalyzerStateMap, key, CreateLanguageSpecificAnalyzerMap, (solution.Analyzers, referenceIdsToRedirect));
return hostAnalyzerInfo.WithExcludedAnalyzers(projectAnalyzerInfo.SkippedAnalyzersInfo.SkippedAnalyzers);

static HostAnalyzerInfo CreateLanguageSpecificAnalyzerMap(HostAnalyzerInfoKey arg, (HostDiagnosticAnalyzers HostAnalyzers, ImmutableHashSet<object> ReferenceIdsToRedirect) state)
Expand Down Expand Up @@ -68,14 +67,14 @@ static HostAnalyzerInfo CreateLanguageSpecificAnalyzerMap(HostAnalyzerInfoKey ar
}

private static ImmutableHashSet<object> GetReferenceIdsToRedirectAsProjectAnalyzers(
Project project)
SolutionState solution, ProjectState project)
{
if (project.State.HasSdkCodeStyleAnalyzers)
if (project.HasSdkCodeStyleAnalyzers)
{
// When a project uses CodeStyle analyzers added by the SDK, we remove them in favor of the
// Features analyzers. We need to then treat the Features analyzers as Project analyzers so
// they do not get access to the Host fallback options.
return GetFeaturesAnalyzerReferenceIds(project.Solution.SolutionState.Analyzers);
return GetFeaturesAnalyzerReferenceIds(solution.Analyzers);
}

return [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ internal ProjectAnalyzerInfo(
}
}

private ProjectAnalyzerInfo? TryGetProjectAnalyzerInfo(Project project)
private ProjectAnalyzerInfo? TryGetProjectAnalyzerInfo(ProjectState project)
{
// check if the analyzer references have changed since the last time we updated the map:
// No need to use _projectAnalyzerStateMapGuard during reads of _projectAnalyzerStateMap
Expand All @@ -54,18 +54,18 @@ internal ProjectAnalyzerInfo(
return null;
}

private async Task<ProjectAnalyzerInfo> GetOrCreateProjectAnalyzerInfoAsync(Project project, CancellationToken cancellationToken)
=> TryGetProjectAnalyzerInfo(project) ?? await UpdateProjectAnalyzerInfoAsync(project, cancellationToken).ConfigureAwait(false);
private async Task<ProjectAnalyzerInfo> GetOrCreateProjectAnalyzerInfoAsync(SolutionState solution, ProjectState project, CancellationToken cancellationToken)
=> TryGetProjectAnalyzerInfo(project) ?? await UpdateProjectAnalyzerInfoAsync(solution, project, cancellationToken).ConfigureAwait(false);

private ProjectAnalyzerInfo CreateProjectAnalyzerInfo(Project project)
private ProjectAnalyzerInfo CreateProjectAnalyzerInfo(SolutionState solution, ProjectState project)
{
if (project.AnalyzerReferences.Count == 0)
{
return ProjectAnalyzerInfo.Default;
}

var hostAnalyzers = project.Solution.SolutionState.Analyzers;
var analyzersPerReference = hostAnalyzers.CreateProjectDiagnosticAnalyzersPerReference(project);
var solutionAnalyzers = solution.Analyzers;
var analyzersPerReference = solutionAnalyzers.CreateProjectDiagnosticAnalyzersPerReference(project);
if (analyzersPerReference.Count == 0)
{
return ProjectAnalyzerInfo.Default;
Expand All @@ -78,15 +78,15 @@ private ProjectAnalyzerInfo CreateProjectAnalyzerInfo(Project project)
// workspace placeholder analyzers. So we should never get host analyzers back here.
Contract.ThrowIfTrue(newHostAnalyzers.Count > 0);

var skippedAnalyzersInfo = hostAnalyzers.GetSkippedAnalyzersInfo(project.State, _analyzerInfoCache);
var skippedAnalyzersInfo = solutionAnalyzers.GetSkippedAnalyzersInfo(project, _analyzerInfoCache);
return new ProjectAnalyzerInfo(project.AnalyzerReferences, newAllAnalyzers, skippedAnalyzersInfo);
}

/// <summary>
/// Updates the map to the given project snapshot.
/// </summary>
private async Task<ProjectAnalyzerInfo> UpdateProjectAnalyzerInfoAsync(
Project project, CancellationToken cancellationToken)
SolutionState solution, ProjectState project, CancellationToken cancellationToken)
{
// This code is called concurrently for a project, so the guard prevents duplicated effort calculating StateSets.
using (await _projectAnalyzerStateMapGuard.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
Expand All @@ -95,7 +95,7 @@ private async Task<ProjectAnalyzerInfo> UpdateProjectAnalyzerInfoAsync(

if (projectAnalyzerInfo == null)
{
projectAnalyzerInfo = CreateProjectAnalyzerInfo(project);
projectAnalyzerInfo = CreateProjectAnalyzerInfo(solution, project);

// update cache.
_projectAnalyzerStateMap = _projectAnalyzerStateMap.SetItem(project.Id, projectAnalyzerInfo.Value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Diagnostics;

Expand Down Expand Up @@ -48,18 +44,18 @@ private partial class StateManager(DiagnosticAnalyzerInfoCache analyzerInfoCache
/// Return <see cref="DiagnosticAnalyzer"/>s for the given <see cref="Project"/>.
/// </summary>
public async Task<ImmutableArray<DiagnosticAnalyzer>> GetOrCreateAnalyzersAsync(
Project project, CancellationToken cancellationToken)
SolutionState solution, ProjectState project, CancellationToken cancellationToken)
{
var hostAnalyzerInfo = await GetOrCreateHostAnalyzerInfoAsync(project, cancellationToken).ConfigureAwait(false);
var projectAnalyzerInfo = await GetOrCreateProjectAnalyzerInfoAsync(project, cancellationToken).ConfigureAwait(false);
var hostAnalyzerInfo = await GetOrCreateHostAnalyzerInfoAsync(solution, project, cancellationToken).ConfigureAwait(false);
var projectAnalyzerInfo = await GetOrCreateProjectAnalyzerInfoAsync(solution, project, cancellationToken).ConfigureAwait(false);
return hostAnalyzerInfo.OrderedAllAnalyzers.AddRange(projectAnalyzerInfo.Analyzers);
}

public async Task<HostAnalyzerInfo> GetOrCreateHostAnalyzerInfoAsync(
Project project, CancellationToken cancellationToken)
SolutionState solution, ProjectState project, CancellationToken cancellationToken)
{
var projectAnalyzerInfo = await GetOrCreateProjectAnalyzerInfoAsync(project, cancellationToken).ConfigureAwait(false);
return GetOrCreateHostAnalyzerInfo(project, projectAnalyzerInfo);
var projectAnalyzerInfo = await GetOrCreateProjectAnalyzerInfoAsync(solution, project, cancellationToken).ConfigureAwait(false);
return GetOrCreateHostAnalyzerInfo(solution, project, projectAnalyzerInfo);
}

private static (ImmutableHashSet<DiagnosticAnalyzer> hostAnalyzers, ImmutableHashSet<DiagnosticAnalyzer> allAnalyzers) PartitionAnalyzers(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public DiagnosticIncrementalAnalyzer(
internal DiagnosticAnalyzerInfoCache DiagnosticAnalyzerInfoCache => _diagnosticAnalyzerRunner.AnalyzerInfoCache;

public Task<ImmutableArray<DiagnosticAnalyzer>> GetAnalyzersForTestingPurposesOnlyAsync(Project project, CancellationToken cancellationToken)
=> _stateManager.GetOrCreateAnalyzersAsync(project, cancellationToken);
=> _stateManager.GetOrCreateAnalyzersAsync(project.Solution.SolutionState, project.State, cancellationToken);

private static string GetProjectLogMessage(Project project, ImmutableArray<DiagnosticAnalyzer> analyzers)
=> $"project: ({project.Id}), ({string.Join(Environment.NewLine, analyzers.Select(a => a.ToString()))})";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ private async Task<ImmutableArray<DiagnosticData>> ProduceProjectDiagnosticsAsyn
{
using var _ = ArrayBuilder<DiagnosticData>.GetInstance(out var builder);

var analyzersForProject = await _stateManager.GetOrCreateAnalyzersAsync(project, cancellationToken).ConfigureAwait(false);
var hostAnalyzerInfo = await _stateManager.GetOrCreateHostAnalyzerInfoAsync(project, cancellationToken).ConfigureAwait(false);
var solution = project.Solution;
var analyzersForProject = await _stateManager.GetOrCreateAnalyzersAsync(
solution.SolutionState, project.State, cancellationToken).ConfigureAwait(false);
var hostAnalyzerInfo = await _stateManager.GetOrCreateHostAnalyzerInfoAsync(
solution.SolutionState, project.State, cancellationToken).ConfigureAwait(false);
var analyzers = analyzersForProject.WhereAsArray(a => ShouldIncludeAnalyzer(project, a));

var result = await GetOrComputeDiagnosticAnalysisResultsAsync(analyzers).ConfigureAwait(false);
Expand Down Expand Up @@ -108,17 +111,19 @@ async Task<ImmutableDictionary<DiagnosticAnalyzer, DiagnosticAnalysisResult>> Ge
// If there was a 'ForceAnalyzeProjectAsync' run for this project, we can piggy back off of the
// prior computed/cached results as they will be a superset of the results we want.
//
// Note: the caller will loop over *its* analzyers, grabbing from the full set of data we've cached
// for this project, and filtering down further. So it's ok to return this potentially larger set.
// Note: the caller will loop over *its* analyzers, grabbing from the full set of data we've cached for
// this project, and filtering down further. So it's ok to return this potentially larger set.
//
// Note: While ForceAnalyzeProjectAsync should always run with a larger set of analyzers than us
// (since it runs all analyzers), we still run a paranoia check that the analyzers we care about are
// a subset of that call so that we don't accidentally reuse results that would not correspond to
// what we are computing ourselves.
if (_projectToForceAnalysisData.TryGetValue(project, out var box) &&
if (s_projectToForceAnalysisData.TryGetValue(project.State, out var box) &&
analyzers.IsSubsetOf(box.Value.analyzers))
{
return box.Value.diagnosticAnalysisResults;
var checksum = await project.GetDependentChecksumAsync(cancellationToken).ConfigureAwait(false);
if (box.Value.checksum == checksum)
return box.Value.diagnosticAnalysisResults;
}

// Otherwise, just compute for the analyzers we care about.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,14 @@ public async Task<ImmutableArray<DiagnosticData>> GetDiagnosticsForSpanAsync(
{
var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);

var project = document.Project;
var solutionState = project.Solution.SolutionState;
var unfilteredAnalyzers = await _stateManager
.GetOrCreateAnalyzersAsync(document.Project, cancellationToken)
.GetOrCreateAnalyzersAsync(solutionState, project.State, cancellationToken)
.ConfigureAwait(false);
var analyzers = unfilteredAnalyzers
.WhereAsArray(a => DocumentAnalysisExecutor.IsAnalyzerEnabledForProject(a, document.Project, GlobalOptions));
var hostAnalyzerInfo = await _stateManager.GetOrCreateHostAnalyzerInfoAsync(document.Project, cancellationToken).ConfigureAwait(false);
var hostAnalyzerInfo = await _stateManager.GetOrCreateHostAnalyzerInfoAsync(solutionState, project.State, cancellationToken).ConfigureAwait(false);

// Note that some callers, such as diagnostic tagger, might pass in a range equal to the entire document span.
// We clear out range for such cases as we are computing full document diagnostics.
Expand Down Expand Up @@ -229,7 +231,7 @@ async Task ComputeDocumentDiagnosticsAsync(

analyzers = filteredAnalyzers.ToImmutable();

var hostAnalyzerInfo = await _stateManager.GetOrCreateHostAnalyzerInfoAsync(document.Project, cancellationToken).ConfigureAwait(false);
var hostAnalyzerInfo = await _stateManager.GetOrCreateHostAnalyzerInfoAsync(solutionState, project.State, cancellationToken).ConfigureAwait(false);

var projectAnalyzers = analyzers.WhereAsArray(static (a, info) => !info.IsHostAnalyzer(a), hostAnalyzerInfo);
var hostAnalyzers = analyzers.WhereAsArray(static (a, info) => info.IsHostAnalyzer(a), hostAnalyzerInfo);
Expand Down
Loading
Loading