diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs index 09efb8642d42b..39cf98993dda9 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs @@ -18,13 +18,13 @@ namespace Microsoft.CodeAnalysis.Diagnostics; internal partial class DiagnosticAnalyzerService { /// - /// Cached data from a to the last instance created - /// for it. Note: the CompilationWithAnalyzersPair instance is dependent on the set of to the last instance + /// created for it. Note: the CompilationWithAnalyzersPair instance is dependent on the set of 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. /// - private static readonly ConditionalWeakTable analyzers, CompilationWithAnalyzersPair? compilationWithAnalyzersPair)>> s_projectToCompilationWithAnalyzers = new(); + private static readonly ConditionalWeakTable analyzers, CompilationWithAnalyzersPair? compilationWithAnalyzersPair)>> s_projectToCompilationWithAnalyzers = new(); private static async Task GetOrCreateCompilationWithAnalyzersAsync( Project project, @@ -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 } @@ -63,13 +67,12 @@ internal partial class DiagnosticAnalyzerService // // Should only be called on a that . // - async Task 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()); @@ -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) @@ -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, @@ -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); - } } diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.HostAnalyzerInfo.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.HostAnalyzerInfo.cs index 292ac0a5d21d9..9a917b7ff7ef5 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.HostAnalyzerInfo.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.HostAnalyzerInfo.cs @@ -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 ReferenceIdsToRedirect) state) @@ -65,14 +66,15 @@ static HostAnalyzerInfo CreateLanguageSpecificAnalyzerMap(HostAnalyzerInfoKey ar } } - private static ImmutableHashSet GetReferenceIdsToRedirectAsProjectAnalyzers(Project project) + private static ImmutableHashSet 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 []; diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.ProjectStates.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.ProjectStates.cs index c3eca357edf17..849e5821bdc8f 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.ProjectStates.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.ProjectStates.cs @@ -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 @@ -54,17 +54,17 @@ internal ProjectAnalyzerInfo( return null; } - private async Task GetOrCreateProjectAnalyzerInfoAsync(Project project, CancellationToken cancellationToken) - => TryGetProjectAnalyzerInfo(project) ?? await UpdateProjectAnalyzerInfoAsync(project, cancellationToken).ConfigureAwait(false); + private async Task 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) { @@ -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); } /// /// Updates the map to the given project snapshot. /// - private async Task UpdateProjectAnalyzerInfoAsync(Project project, CancellationToken cancellationToken) + private async Task 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)) @@ -94,7 +95,7 @@ private async Task UpdateProjectAnalyzerInfoAsync(Project p if (projectAnalyzerInfo == null) { - projectAnalyzerInfo = CreateProjectAnalyzerInfo(project); + projectAnalyzerInfo = CreateProjectAnalyzerInfo(solution, project); // update cache. _projectAnalyzerStateMap = _projectAnalyzerStateMap.SetItem(project.Id, projectAnalyzerInfo.Value); diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.cs index cd07e28f14daf..11712ea77dd66 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.cs @@ -47,17 +47,19 @@ private partial class StateManager(DiagnosticAnalyzerInfoCache analyzerInfoCache /// /// Return s for the given . /// - public async Task> GetOrCreateAnalyzersAsync(Project project, CancellationToken cancellationToken) + public async Task> 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 GetOrCreateHostAnalyzerInfoAsync(Project project, CancellationToken cancellationToken) + public async Task 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 hostAnalyzers, ImmutableHashSet allAnalyzers) PartitionAnalyzers( diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs index e968d7d55b57a..65beecdfe360d 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs @@ -50,7 +50,7 @@ public DiagnosticIncrementalAnalyzer( internal DiagnosticAnalyzerInfoCache DiagnosticAnalyzerInfoCache => _diagnosticAnalyzerRunner.AnalyzerInfoCache; public Task> GetAnalyzersForTestingPurposesOnlyAsync(Project project, CancellationToken cancellationToken) - => _stateManager.GetOrCreateAnalyzersAsync(project, cancellationToken); + => _stateManager.GetOrCreateAnalyzersAsync(project.Solution.SolutionState, project.State, cancellationToken); private static string GetProjectLogMessage(Project project, ImmutableArray analyzers) => $"project: ({project.Id}), ({string.Join(Environment.NewLine, analyzers.Select(a => a.ToString()))})"; diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs index 5d6c9799b1038..4df57f00f44d9 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs @@ -60,8 +60,11 @@ private async Task> ProduceProjectDiagnosticsAsyn { using var _ = ArrayBuilder.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); @@ -108,14 +111,16 @@ async Task> 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; diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs index f53626179aa28..48da4ffee222a 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs @@ -59,12 +59,14 @@ public async Task> 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. @@ -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); diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs index 99147ea7972d1..93490ccb00f0d 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs @@ -20,34 +20,46 @@ internal partial class DiagnosticAnalyzerService private partial class DiagnosticIncrementalAnalyzer { /// - /// Cached data from a real instance to the cached diagnostic data produced by + /// Cached data from a real instance to the cached diagnostic data produced by /// all the analyzers for the project. This data can then be used by to speed up subsequent calls through the normal entry points as long as the project hasn't changed at all. /// - private readonly ConditionalWeakTable analyzers, ImmutableDictionary diagnosticAnalysisResults)>> _projectToForceAnalysisData = new(); + /// + /// This table is keyed off of but stores data from on + /// it. Specifically . Normally keying off a ProjectState would not be ok + /// as the ProjectState might stay the same while the SolutionState changed. However, that can't happen as + /// SolutionState has the data for Analyzers computed prior to Projects being added, and then never changes. + /// Practically, solution analyzers are the core Roslyn analyzers themselves we distribute, or analyzers shipped + /// by vsix (not nuget). These analyzers do not get loaded after changing *until* VS restarts. + /// + private static readonly ConditionalWeakTable analyzers, ImmutableDictionary diagnosticAnalysisResults)>> _projectToForceAnalysisData = new(); public async Task> ForceAnalyzeProjectAsync(Project project, CancellationToken cancellationToken) { + var projectState = project.State; + var checksum = await project.GetDependentChecksumAsync(cancellationToken).ConfigureAwait(false); + try { - if (!_projectToForceAnalysisData.TryGetValue(project, out var box)) + if (!_projectToForceAnalysisData.TryGetValue(projectState, out var box) || + box.Value.checksum != checksum) { box = new(await ComputeForceAnalyzeProjectAsync().ConfigureAwait(false)); // Try to add the new computed data to the CWT. But use any existing value that another thread // might have beaten us to storing in it. #if NET - _projectToForceAnalysisData.TryAdd(project, box); - Contract.ThrowIfFalse(_projectToForceAnalysisData.TryGetValue(project, out box)); + if (!_projectToForceAnalysisData.TryAdd(projectState, box)) + Contract.ThrowIfFalse(_projectToForceAnalysisData.TryGetValue(projectState, out box)); #else - box = _projectToForceAnalysisData.GetValue(project, _ => box); + box = _projectToForceAnalysisData.GetValue(projectState, _ => box); #endif } using var _ = ArrayBuilder.GetInstance(out var diagnostics); - var (analyzers, projectAnalysisData) = box.Value; + var (_, analyzers, projectAnalysisData) = box.Value; foreach (var analyzer in analyzers) { if (projectAnalysisData.TryGetValue(analyzer, out var analyzerResult)) @@ -61,10 +73,11 @@ public async Task> ForceAnalyzeProjectAsync(Proje throw ExceptionUtilities.Unreachable(); } - async Task<(ImmutableArray analyzers, ImmutableDictionary diagnosticAnalysisResults)> ComputeForceAnalyzeProjectAsync() + async Task<(Checksum checksum, ImmutableArray analyzers, ImmutableDictionary diagnosticAnalysisResults)> ComputeForceAnalyzeProjectAsync() { - var allAnalyzers = await _stateManager.GetOrCreateAnalyzersAsync(project, cancellationToken).ConfigureAwait(false); - var hostAnalyzerInfo = await _stateManager.GetOrCreateHostAnalyzerInfoAsync(project, cancellationToken).ConfigureAwait(false); + var solutionState = project.Solution.SolutionState; + var allAnalyzers = await _stateManager.GetOrCreateAnalyzersAsync(solutionState, projectState, cancellationToken).ConfigureAwait(false); + var hostAnalyzerInfo = await _stateManager.GetOrCreateHostAnalyzerInfoAsync(solutionState, projectState, cancellationToken).ConfigureAwait(false); var fullSolutionAnalysisAnalyzers = allAnalyzers.WhereAsArray( static (analyzer, arg) => IsCandidateForFullSolutionAnalysis( @@ -75,7 +88,7 @@ public async Task> ForceAnalyzeProjectAsync(Proje project, fullSolutionAnalysisAnalyzers, hostAnalyzerInfo, AnalyzerService.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false); var projectAnalysisData = await ComputeDiagnosticAnalysisResultsAsync(compilationWithAnalyzers, project, fullSolutionAnalysisAnalyzers, cancellationToken).ConfigureAwait(false); - return (fullSolutionAnalysisAnalyzers, projectAnalysisData); + return (checksum, fullSolutionAnalysisAnalyzers, projectAnalysisData); } static bool IsCandidateForFullSolutionAnalysis( diff --git a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/InProcOrRemoteHostAnalyzerRunner.cs b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/InProcOrRemoteHostAnalyzerRunner.cs index 1203bb1dabee7..d43c3016d6487 100644 --- a/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/InProcOrRemoteHostAnalyzerRunner.cs +++ b/src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/InProcOrRemoteHostAnalyzerRunner.cs @@ -131,7 +131,7 @@ private async Task.Empty; diff --git a/src/Workspaces/Core/Portable/Diagnostics/HostDiagnosticAnalyzers.cs b/src/Workspaces/Core/Portable/Diagnostics/HostDiagnosticAnalyzers.cs index e63221b427381..fb72eccc2dc01 100644 --- a/src/Workspaces/Core/Portable/Diagnostics/HostDiagnosticAnalyzers.cs +++ b/src/Workspaces/Core/Portable/Diagnostics/HostDiagnosticAnalyzers.cs @@ -133,7 +133,7 @@ public ImmutableDictionary> CreateDia /// Create identity and s map for given that /// has only project analyzers /// - public ImmutableDictionary> CreateProjectDiagnosticAnalyzersPerReference(Project project) + public ImmutableDictionary> CreateProjectDiagnosticAnalyzersPerReference(ProjectState project) => CreateProjectDiagnosticAnalyzersPerReference(project.AnalyzerReferences, project.Language); public ImmutableDictionary> CreateProjectDiagnosticAnalyzersPerReference(IReadOnlyList projectAnalyzerReferences, string language) @@ -296,7 +296,7 @@ private static ImmutableDictionary> M return current; } - public SkippedHostAnalyzersInfo GetSkippedAnalyzersInfo(Project project, DiagnosticAnalyzerInfoCache infoCache) + public SkippedHostAnalyzersInfo GetSkippedAnalyzersInfo(ProjectState project, DiagnosticAnalyzerInfoCache infoCache) { var box = _skippedHostAnalyzers.GetOrCreateValue(project.AnalyzerReferences); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs index c76d53fcca168..260c146144562 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs @@ -830,9 +830,6 @@ internal StructuredAnalyzerConfigOptions GetFallbackAnalyzerOptions() private string GetDebuggerDisplay() => this.Name; - internal SkippedHostAnalyzersInfo GetSkippedAnalyzersInfo(DiagnosticAnalyzerInfoCache infoCache) - => Solution.SolutionState.Analyzers.GetSkippedAnalyzersInfo(this, infoCache); - internal async ValueTask GetDocumentAsync(ImmutableArray contentHash, CancellationToken cancellationToken) { var documentId = await State.GetDocumentIdAsync(contentHash, cancellationToken).ConfigureAwait(false); diff --git a/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/DiagnosticComputer.cs b/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/DiagnosticComputer.cs index 3a767508ac893..6ff32cf4ac6e1 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/DiagnosticComputer.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/DiagnosticComputer.cs @@ -356,7 +356,8 @@ private async Task GetDiagnosticsAsync( } } - var skippedAnalyzersInfo = _project.GetSkippedAnalyzersInfo(_analyzerInfoCache); + var skippedAnalyzersInfo = _project.Solution.SolutionState.Analyzers.GetSkippedAnalyzersInfo( + _project.State, _analyzerInfoCache); return await AnalyzeAsync(compilationWithAnalyzers, analyzerToIdMap, projectAnalyzers, hostAnalyzers, skippedAnalyzersInfo, logPerformanceInfo, getTelemetryInfo, cancellationToken).ConfigureAwait(false);