Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ namespace Microsoft.CodeAnalysis.Diagnostics;
internal partial class DiagnosticAnalyzerService
{
/// <summary>
/// Cached data from a <see cref="Project"/> to the last <see cref="CompilationWithAnalyzersPair"/> instance created
/// for it. Note: the CompilationWithAnalyzersPair instance is dependent on the set of <see
/// Cached data from a <see cref="ProjectState"/> to the last <see cref="CompilationWithAnalyzersPair"/> instance
/// created for it. Note: the CompilationWithAnalyzersPair instance is dependent on the set of <see
/// cref="DiagnosticAnalyzer"/>s passed along with the project. As such, we might not be able to use a prior cached
/// value if the set of analyzers changes. In that case, a new instance will be created and will be cached for the
/// next caller.
/// </summary>
private static readonly ConditionalWeakTable<Project, StrongBox<(ImmutableArray<DiagnosticAnalyzer> analyzers, CompilationWithAnalyzersPair? compilationWithAnalyzersPair)>> s_projectToCompilationWithAnalyzers = new();
private static readonly ConditionalWeakTable<ProjectState, StrongBox<(Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers, CompilationWithAnalyzersPair? compilationWithAnalyzersPair)>> s_projectToCompilationWithAnalyzers = new();

private static async Task<CompilationWithAnalyzersPair?> GetOrCreateCompilationWithAnalyzersAsync(
Project project,
Expand All @@ -36,25 +36,29 @@ internal partial class DiagnosticAnalyzerService
if (!project.SupportsCompilation)
return null;

var projectState = project.State;
var checksum = await project.GetDependentChecksumAsync(cancellationToken).ConfigureAwait(false);

// Make sure the cached pair was computed with at least the same state sets we're asking about. if not,
// recompute and cache with the new state sets.
if (!s_projectToCompilationWithAnalyzers.TryGetValue(project, out var tupleBox) ||
if (!s_projectToCompilationWithAnalyzers.TryGetValue(projectState, out var tupleBox) ||
!analyzers.IsSubsetOf(tupleBox.Value.analyzers))
{
var compilationWithAnalyzersPair = await CreateCompilationWithAnalyzersAsync().ConfigureAwait(false);
tupleBox = new((analyzers, compilationWithAnalyzersPair));
var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
var compilationWithAnalyzersPair = CreateCompilationWithAnalyzers(projectState, compilation);
tupleBox = new((checksum, analyzers, compilationWithAnalyzersPair));

#if NET
s_projectToCompilationWithAnalyzers.AddOrUpdate(project, tupleBox);
s_projectToCompilationWithAnalyzers.AddOrUpdate(projectState, tupleBox);
#else
// Make a best effort attempt to store the latest computed value against these state sets. If this
// fails (because another thread interleaves with this), that's ok. We still return the pair we
// computed, so our caller will still see the right data
s_projectToCompilationWithAnalyzers.Remove(project);
s_projectToCompilationWithAnalyzers.Remove(projectState);

// Intentionally ignore the result of this. We still want to use the value we computed above, even if
// another thread interleaves and sets a different value.
s_projectToCompilationWithAnalyzers.GetValue(project, _ => tupleBox);
s_projectToCompilationWithAnalyzers.GetValue(projectState, _ => tupleBox);
#endif
}

Expand All @@ -63,13 +67,12 @@ internal partial class DiagnosticAnalyzerService
// <summary>
// Should only be called on a <see cref="Project"/> that <see cref="Project.SupportsCompilation"/>.
// </summary>
async Task<CompilationWithAnalyzersPair?> CreateCompilationWithAnalyzersAsync()
CompilationWithAnalyzersPair? CreateCompilationWithAnalyzers(
ProjectState project, Compilation compilation)
{
var projectAnalyzers = analyzers.WhereAsArray(static (s, info) => !info.IsHostAnalyzer(s), hostAnalyzerInfo);
var hostAnalyzers = analyzers.WhereAsArray(static (s, info) => info.IsHostAnalyzer(s), hostAnalyzerInfo);

var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);

// Create driver that holds onto compilation and associated analyzers
var filteredProjectAnalyzers = projectAnalyzers.WhereAsArray(static a => !a.IsWorkspaceDiagnosticAnalyzer());
var filteredHostAnalyzers = hostAnalyzers.WhereAsArray(static a => !a.IsWorkspaceDiagnosticAnalyzer());
Expand All @@ -83,9 +86,6 @@ internal partial class DiagnosticAnalyzerService
return null;
}

Contract.ThrowIfFalse(project.SupportsCompilation);
AssertCompilation(project, compilation);

var exceptionFilter = (Exception ex) =>
{
if (ex is not OperationCanceledException && crashOnAnalyzerException)
Expand All @@ -105,7 +105,7 @@ internal partial class DiagnosticAnalyzerService
var projectCompilation = !filteredProjectAnalyzers.Any()
? null
: compilation.WithAnalyzers(filteredProjectAnalyzers, new CompilationWithAnalyzersOptions(
options: project.AnalyzerOptions,
options: project.ProjectAnalyzerOptions,
onAnalyzerException: null,
analyzerExceptionFilter: exceptionFilter,
concurrentAnalysis: false,
Expand All @@ -126,12 +126,4 @@ internal partial class DiagnosticAnalyzerService
return new CompilationWithAnalyzersPair(projectCompilation, hostCompilation);
}
}

[Conditional("DEBUG")]
private static void AssertCompilation(Project project, Compilation compilation1)
{
// given compilation must be from given project.
Contract.ThrowIfFalse(project.TryGetCompilation(out var compilation2));
Contract.ThrowIfFalse(compilation1 == compilation2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ private partial class DiagnosticIncrementalAnalyzer
{
private partial class StateManager
{
private HostAnalyzerInfo GetOrCreateHostAnalyzerInfo(Project project, ProjectAnalyzerInfo projectAnalyzerInfo)
private HostAnalyzerInfo GetOrCreateHostAnalyzerInfo(
SolutionState solution, ProjectState project, ProjectAnalyzerInfo projectAnalyzerInfo)
{
var key = new HostAnalyzerInfoKey(project.Language, project.State.HasSdkCodeStyleAnalyzers, project.Solution.SolutionState.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, (project.Solution.SolutionState.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 @@ -65,14 +66,15 @@ static HostAnalyzerInfo CreateLanguageSpecificAnalyzerMap(HostAnalyzerInfoKey ar
}
}

private static ImmutableHashSet<object> GetReferenceIdsToRedirectAsProjectAnalyzers(Project project)
private static ImmutableHashSet<object> GetReferenceIdsToRedirectAsProjectAnalyzers(
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,17 +54,17 @@ 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 hostAnalyzers = solution.Analyzers;
var analyzersPerReference = hostAnalyzers.CreateProjectDiagnosticAnalyzersPerReference(project);
if (analyzersPerReference.Count == 0)
{
Expand All @@ -78,14 +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 = project.GetSkippedAnalyzersInfo(_analyzerInfoCache);
var skippedAnalyzersInfo = solution.Analyzers.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)
private async Task<ProjectAnalyzerInfo> UpdateProjectAnalyzerInfoAsync(
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 @@ -94,7 +95,7 @@ private async Task<ProjectAnalyzerInfo> UpdateProjectAnalyzerInfoAsync(Project p

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 @@ -47,17 +47,19 @@ private partial class StateManager(DiagnosticAnalyzerInfoCache analyzerInfoCache
/// <summary>
/// Return <see cref="DiagnosticAnalyzer"/>s for the given <see cref="Project"/>.
/// </summary>
public async Task<ImmutableArray<DiagnosticAnalyzer>> GetOrCreateAnalyzersAsync(Project project, CancellationToken cancellationToken)
public async Task<ImmutableArray<DiagnosticAnalyzer>> GetOrCreateAnalyzersAsync(
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)
public async Task<HostAnalyzerInfo> GetOrCreateHostAnalyzerInfoAsync(
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,14 +111,16 @@ 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) &&
var checksum = await project.GetDependentChecksumAsync(cancellationToken).ConfigureAwait(false);
if (_projectToForceAnalysisData.TryGetValue(project.State, out var box) &&
box.Value.checksum == checksum &&
analyzers.IsSubsetOf(box.Value.analyzers))
{
return box.Value.diagnosticAnalysisResults;
Expand Down
Loading