File: Features\Diagnostics\EngineV2\DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs
Web Access
Project: src\src\LanguageServer\Protocol\Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol)
// Licensed to the .NET Foundation under one or more agreements.
// 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.Immutable;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Workspaces.Diagnostics;
 
namespace Microsoft.CodeAnalysis.Diagnostics;
 
internal partial class DiagnosticAnalyzerService
{
    private partial class DiagnosticIncrementalAnalyzer
    {
        /// <summary>
        /// Cached data from a real <see cref="ProjectState"/> instance to the cached diagnostic data produced by
        /// <em>all</em> the analyzers for the project.  This data can then be used by <see
        /// cref="GetDiagnosticsForIdsAsync"/> to speed up subsequent calls through the normal <see
        /// cref="IDiagnosticAnalyzerService"/> entry points as long as the project hasn't changed at all.
        /// </summary>
        /// <remarks>
        /// This table is keyed off of <see cref="ProjectState"/> but stores data from <see cref="SolutionState"/> on
        /// it.  Specifically <see cref="SolutionState.Analyzers"/>.  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.
        /// </remarks>
        private static readonly ConditionalWeakTable<ProjectState, StrongBox<(Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers, ImmutableDictionary<DiagnosticAnalyzer, DiagnosticAnalysisResult> diagnosticAnalysisResults)>> s_projectToForceAnalysisData = new();
 
        public async Task<ImmutableArray<DiagnosticData>> ForceAnalyzeProjectAsync(Project project, CancellationToken cancellationToken)
        {
            var projectState = project.State;
            var checksum = await project.GetDiagnosticChecksumAsync(cancellationToken).ConfigureAwait(false);
 
            try
            {
                if (!s_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
                    if (!s_projectToForceAnalysisData.TryAdd(projectState, box))
                        Contract.ThrowIfFalse(s_projectToForceAnalysisData.TryGetValue(projectState, out box));
#else
                    box = s_projectToForceAnalysisData.GetValue(projectState, _ => box);
#endif
                }
 
                using var _ = ArrayBuilder<DiagnosticData>.GetInstance(out var diagnostics);
 
                var (_, analyzers, projectAnalysisData) = box.Value;
                foreach (var analyzer in analyzers)
                {
                    if (projectAnalysisData.TryGetValue(analyzer, out var analyzerResult))
                        diagnostics.AddRange(analyzerResult.GetAllDiagnostics());
                }
 
                return diagnostics.ToImmutableAndClear();
            }
            catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
            {
                throw ExceptionUtilities.Unreachable();
            }
 
            async Task<(Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers, ImmutableDictionary<DiagnosticAnalyzer, DiagnosticAnalysisResult> diagnosticAnalysisResults)> ComputeForceAnalyzeProjectAsync()
            {
                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(
                        arg.self.DiagnosticAnalyzerInfoCache, analyzer, arg.hostAnalyzerInfo.IsHostAnalyzer(analyzer), arg.project),
                    (self: this, project, hostAnalyzerInfo));
 
                var compilationWithAnalyzers = await GetOrCreateCompilationWithAnalyzersAsync(
                    project, fullSolutionAnalysisAnalyzers, hostAnalyzerInfo, AnalyzerService.CrashOnAnalyzerException, cancellationToken).ConfigureAwait(false);
 
                var projectAnalysisData = await ComputeDiagnosticAnalysisResultsAsync(compilationWithAnalyzers, project, fullSolutionAnalysisAnalyzers, cancellationToken).ConfigureAwait(false);
                return (checksum, fullSolutionAnalysisAnalyzers, projectAnalysisData);
            }
 
            static bool IsCandidateForFullSolutionAnalysis(
                DiagnosticAnalyzerInfoCache infoCache, DiagnosticAnalyzer analyzer, bool isHostAnalyzer, Project project)
            {
                // PERF: Don't query descriptors for compiler analyzer or workspace load analyzer, always execute them.
                if (analyzer == FileContentLoadAnalyzer.Instance ||
                    analyzer == GeneratorDiagnosticsPlaceholderAnalyzer.Instance ||
                    analyzer.IsCompilerAnalyzer())
                {
                    return true;
                }
 
                if (analyzer.IsBuiltInAnalyzer())
                {
                    // always return true for builtin analyzer. we can't use
                    // descriptor check since many builtin analyzer always return 
                    // hidden descriptor regardless what descriptor it actually
                    // return on runtime. they do this so that they can control
                    // severity through option page rather than rule set editor.
                    // this is special behavior only ide analyzer can do. we hope
                    // once we support editorconfig fully, third party can use this
                    // ability as well and we can remove this kind special treatment on builtin
                    // analyzer.
                    return true;
                }
 
                if (analyzer is DiagnosticSuppressor)
                {
                    // Always execute diagnostic suppressors.
                    return true;
                }
 
                if (project.CompilationOptions is null)
                {
                    // Skip compilation options based checks for non-C#/VB projects.
                    return true;
                }
 
                // For most of analyzers, the number of diagnostic descriptors is small, so this should be cheap.
                var descriptors = infoCache.GetDiagnosticDescriptors(analyzer);
                var analyzerConfigOptions = project.GetAnalyzerConfigOptions();
 
                return descriptors.Any(static (d, arg) => d.GetEffectiveSeverity(arg.CompilationOptions, arg.isHostAnalyzer ? arg.analyzerConfigOptions?.ConfigOptionsWithFallback : arg.analyzerConfigOptions?.ConfigOptionsWithoutFallback, arg.analyzerConfigOptions?.TreeOptions) != ReportDiagnostic.Hidden, (project.CompilationOptions, isHostAnalyzer, analyzerConfigOptions));
            }
        }
    }
}