File: Features\Diagnostics\EngineV2\DiagnosticIncrementalAnalyzer.CompilationManager.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.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Diagnostics;
 
internal partial class DiagnosticAnalyzerService
{
    /// <summary>
    /// 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<ProjectState, StrongBox<(Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers, CompilationWithAnalyzersPair? compilationWithAnalyzersPair)>> s_projectToCompilationWithAnalyzers = new();
 
    private static async Task<CompilationWithAnalyzersPair?> GetOrCreateCompilationWithAnalyzersAsync(
        Project project,
        ImmutableArray<DiagnosticAnalyzer> analyzers,
        HostAnalyzerInfo hostAnalyzerInfo,
        bool crashOnAnalyzerException,
        CancellationToken cancellationToken)
    {
        if (!project.SupportsCompilation)
            return null;
 
        var projectState = project.State;
        var checksum = await project.GetDiagnosticChecksumAsync(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(projectState, out var tupleBox) ||
            tupleBox.Value.checksum != checksum ||
            !analyzers.IsSubsetOf(tupleBox.Value.analyzers))
        {
            var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
            var compilationWithAnalyzersPair = CreateCompilationWithAnalyzers(projectState, compilation);
            tupleBox = new((checksum, analyzers, compilationWithAnalyzersPair));
 
#if NET
            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(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(projectState, _ => tupleBox);
#endif
        }
 
        return tupleBox.Value.compilationWithAnalyzersPair;
 
        // <summary>
        // Should only be called on a <see cref="Project"/> that <see cref="Project.SupportsCompilation"/>.
        // </summary>
        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);
 
            // 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());
            var filteredProjectSuppressors = filteredProjectAnalyzers.WhereAsArray(static a => a is DiagnosticSuppressor);
            filteredHostAnalyzers = filteredHostAnalyzers.AddRange(filteredProjectSuppressors);
 
            // PERF: there is no analyzers for this compilation.
            //       compilationWithAnalyzer will throw if it is created with no analyzers which is perf optimization.
            if (filteredProjectAnalyzers.IsEmpty && filteredHostAnalyzers.IsEmpty)
            {
                return null;
            }
 
            var exceptionFilter = (Exception ex) =>
            {
                if (ex is not OperationCanceledException && crashOnAnalyzerException)
                {
                    // report telemetry
                    FatalError.ReportAndPropagate(ex);
 
                    // force fail fast (the host might not crash when reporting telemetry):
                    FailFast.OnFatalException(ex);
                }
 
                return true;
            };
 
            // in IDE, we always set concurrentAnalysis == false otherwise, we can get into thread starvation due to
            // async being used with synchronous blocking concurrency.
            var projectCompilation = !filteredProjectAnalyzers.Any()
                ? null
                : compilation.WithAnalyzers(filteredProjectAnalyzers, new CompilationWithAnalyzersOptions(
                    options: project.ProjectAnalyzerOptions,
                    onAnalyzerException: null,
                    analyzerExceptionFilter: exceptionFilter,
                    concurrentAnalysis: false,
                    logAnalyzerExecutionTime: true,
                    reportSuppressedDiagnostics: true));
 
            var hostCompilation = !filteredHostAnalyzers.Any()
                ? null
                : compilation.WithAnalyzers(filteredHostAnalyzers, new CompilationWithAnalyzersOptions(
                    options: project.HostAnalyzerOptions,
                    onAnalyzerException: null,
                    analyzerExceptionFilter: exceptionFilter,
                    concurrentAnalysis: false,
                    logAnalyzerExecutionTime: true,
                    reportSuppressedDiagnostics: true));
 
            // Create driver that holds onto compilation and associated analyzers
            return new CompilationWithAnalyzersPair(projectCompilation, hostCompilation);
        }
    }
}