File: Diagnostics\Service\DiagnosticAnalyzerService_CompilationWithAnalyzersPair.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Generic;
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 sealed partial class DiagnosticAnalyzerService
{
    /// <summary>
    /// Cached data from a <see cref="ProjectState"/> to the <see cref="CompilationWithAnalyzersPair"/>s
    /// we've created for it.  Note: the CompilationWithAnalyzersPair instance is dependent on the set of <see
    /// cref="DiagnosticAnalyzer"/>s passed along with the project.
    /// <para/>
    /// The value of the table is a SmallDictionary that maps from the 
    /// <see cref="Project"/> checksum the set of <see cref="DiagnosticAnalyzer"/>s being requested.
    /// Note: this dictionary must be locked with <see cref="s_gate"/> before accessing it.  A 
    /// small dictionary is chosen as this will normally only have one item in it (the current project
    /// and all its analyzers).  Occasionally it will have more, if (for example) a request to run
    /// a single analyzer is performed.
    /// </summary>
    private static readonly ConditionalWeakTable<
        ProjectState,
        SmallDictionary<
            (Checksum checksum, ImmutableArray<DiagnosticAnalyzer> analyzers),
            AsyncLazy<CompilationWithAnalyzersPair?>>> s_projectToCompilationWithAnalyzers = new();
 
    /// <summary>
    /// Protection around the SmallDictionary in <see cref="s_projectToCompilationWithAnalyzers"/>.
    /// </summary>
    private static readonly SemaphoreSlim s_gate = new(initialCount: 1);
 
    private static async Task<CompilationWithAnalyzersPair?> GetOrCreateCompilationWithAnalyzersAsync(
        Project project,
        ImmutableArray<DiagnosticAnalyzer> analyzers,
        HostAnalyzerInfo hostAnalyzerInfo,
        bool crashOnAnalyzerException,
        CancellationToken cancellationToken)
    {
        if (!project.SupportsCompilation)
            return null;
 
        var checksum = await project.GetDiagnosticChecksumAsync(cancellationToken).ConfigureAwait(false);
 
        // Make sure the cached pair was computed with the same state sets we're asking about.  if not,
        // recompute and cache with the new state sets.
        var map = s_projectToCompilationWithAnalyzers.GetValue(
            project.State, static _ => new(ChecksumAndAnalyzersEqualityComparer.Instance));
 
        AsyncLazy<CompilationWithAnalyzersPair?>? lazy;
        using (await s_gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
        {
            var checksumAndAnalyzers = (checksum, analyzers);
            if (!map.TryGetValue(checksumAndAnalyzers, out lazy))
            {
                lazy = AsyncLazy.Create(
                    asynchronousComputeFunction: CreateCompilationWithAnalyzersAsync,
                    arg: (project, analyzers, hostAnalyzerInfo, crashOnAnalyzerException));
                map.Add(checksumAndAnalyzers, lazy);
            }
        }
 
        return await lazy.GetValueAsync(cancellationToken).ConfigureAwait(false);
 
        // <summary>
        // Should only be called on a <see cref="Project"/> that <see cref="Project.SupportsCompilation"/>.
        // </summary>
        static async Task<CompilationWithAnalyzersPair?> CreateCompilationWithAnalyzersAsync(
            (Project project,
             ImmutableArray<DiagnosticAnalyzer> analyzers,
             HostAnalyzerInfo hostAnalyzerInfo,
             bool crashOnAnalyzerException) tuple,
            CancellationToken cancellationToken)
        {
            var (project, analyzers, hostAnalyzerInfo, crashOnAnalyzerException) = tuple;
 
            var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
 
            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;
            };
 
            // Create driver that holds onto compilation and associated analyzers
            return new(
                CreateCompilationWithAnalyzers(compilation, filteredProjectAnalyzers, project.State.ProjectAnalyzerOptions, exceptionFilter),
                CreateCompilationWithAnalyzers(compilation, filteredHostAnalyzers, project.HostAnalyzerOptions, exceptionFilter));
        }
 
        static CompilationWithAnalyzers? CreateCompilationWithAnalyzers(
            Compilation compilation,
            ImmutableArray<DiagnosticAnalyzer> analyzers,
            AnalyzerOptions? options,
            Func<Exception, bool> exceptionFilter)
        {
            if (analyzers.Length == 0)
                return null;
 
            return compilation.WithAnalyzers(analyzers, new CompilationWithAnalyzersOptions(
                options: options,
                onAnalyzerException: null,
                analyzerExceptionFilter: exceptionFilter,
                // in IDE, we always set concurrentAnalysis == false otherwise, we can get into thread starvation due to
                // async being used with synchronous blocking concurrency.
                concurrentAnalysis: false,
                logAnalyzerExecutionTime: true,
                reportSuppressedDiagnostics: true));
        }
    }
}