File: DiagnosticAnalyzer\AnalyzerManager.cs
Web Access
Project: src\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis)
// 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.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Diagnostics
{
    /// <summary>
    /// Manages properties of analyzers (such as registered actions, supported diagnostics) for analyzer host's lifetime
    /// and executes the callbacks into the analyzers.
    /// 
    /// It ensures the following for the lifetime of analyzer host:
    /// 1) <see cref="DiagnosticAnalyzer.Initialize(AnalysisContext)"/> is invoked only once per-analyzer.
    /// 2) <see cref="DiagnosticAnalyzer.SupportedDiagnostics"/> is invoked only once per-analyzer.
    /// 3) <see cref="CompilationStartAnalyzerAction"/> registered during Initialize are invoked only once per-compilation per-analyzer and analyzer options.
    /// </summary>
    internal partial class AnalyzerManager
    {
        // This cache stores the analyzer execution context per-analyzer (i.e. registered actions, supported descriptors, etc.).
        // Not created as ImmutableDictionary for perf considerations, but should be treated as immutable
        private readonly Dictionary<DiagnosticAnalyzer, AnalyzerExecutionContext> _analyzerExecutionContextMap;
 
        public AnalyzerManager(ImmutableArray<DiagnosticAnalyzer> analyzers)
        {
            _analyzerExecutionContextMap = CreateAnalyzerExecutionContextMap(analyzers);
        }
 
        public AnalyzerManager(DiagnosticAnalyzer analyzer)
        {
            _analyzerExecutionContextMap = CreateAnalyzerExecutionContextMap(SpecializedCollections.SingletonEnumerable(analyzer));
        }
 
        private Dictionary<DiagnosticAnalyzer, AnalyzerExecutionContext> CreateAnalyzerExecutionContextMap(IEnumerable<DiagnosticAnalyzer> analyzers)
        {
            var analyzerExecutionContextMap = new Dictionary<DiagnosticAnalyzer, AnalyzerExecutionContext>();
            foreach (var analyzer in analyzers)
            {
                analyzerExecutionContextMap.Add(analyzer, new AnalyzerExecutionContext(analyzer));
            }
 
            return analyzerExecutionContextMap;
        }
 
        private AnalyzerExecutionContext GetAnalyzerExecutionContext(DiagnosticAnalyzer analyzer) => _analyzerExecutionContextMap[analyzer];
 
        [PerformanceSensitive(
            "https://github.com/dotnet/roslyn/issues/26778",
            OftenCompletesSynchronously = true)]
        private async ValueTask<HostCompilationStartAnalysisScope> GetCompilationAnalysisScopeAsync(
            HostSessionStartAnalysisScope sessionScope,
            AnalyzerExecutor analyzerExecutor,
            CancellationToken cancellationToken)
        {
            var analyzerExecutionContext = GetAnalyzerExecutionContext(sessionScope.Analyzer);
            return await GetCompilationAnalysisScopeCoreAsync(sessionScope, analyzerExecutor, analyzerExecutionContext, cancellationToken).ConfigureAwait(false);
        }
 
        [PerformanceSensitive(
            "https://github.com/dotnet/roslyn/issues/26778",
            OftenCompletesSynchronously = true)]
        private async ValueTask<HostCompilationStartAnalysisScope> GetCompilationAnalysisScopeCoreAsync(
            HostSessionStartAnalysisScope sessionScope,
            AnalyzerExecutor analyzerExecutor,
            AnalyzerExecutionContext analyzerExecutionContext,
            CancellationToken cancellationToken)
        {
            try
            {
                return await analyzerExecutionContext.GetCompilationAnalysisScopeAsync(sessionScope, analyzerExecutor, cancellationToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                // Task to compute the scope was cancelled.
                // Clear the compilation scope for analyzer, so we can attempt a retry.
                analyzerExecutionContext.ClearCompilationScopeTask();
 
                cancellationToken.ThrowIfCancellationRequested();
                return await GetCompilationAnalysisScopeCoreAsync(sessionScope, analyzerExecutor, analyzerExecutionContext, cancellationToken).ConfigureAwait(false);
            }
        }
 
        private async Task<HostSymbolStartAnalysisScope> GetSymbolAnalysisScopeAsync(
            ISymbol symbol,
            bool isGeneratedCodeSymbol,
            SyntaxTree? filterTree,
            TextSpan? filterSpan,
            DiagnosticAnalyzer analyzer,
            ImmutableArray<SymbolStartAnalyzerAction> symbolStartActions,
            AnalyzerExecutor analyzerExecutor,
            CancellationToken cancellationToken)
        {
            var analyzerExecutionContext = GetAnalyzerExecutionContext(analyzer);
            return await GetSymbolAnalysisScopeCoreAsync(symbol, isGeneratedCodeSymbol, filterTree, filterSpan, symbolStartActions, analyzerExecutor, analyzerExecutionContext, cancellationToken).ConfigureAwait(false);
        }
 
        private async Task<HostSymbolStartAnalysisScope> GetSymbolAnalysisScopeCoreAsync(
            ISymbol symbol,
            bool isGeneratedCodeSymbol,
            SyntaxTree? filterTree,
            TextSpan? filterSpan,
            ImmutableArray<SymbolStartAnalyzerAction> symbolStartActions,
            AnalyzerExecutor analyzerExecutor,
            AnalyzerExecutionContext analyzerExecutionContext,
            CancellationToken cancellationToken)
        {
            try
            {
                return await analyzerExecutionContext.GetSymbolAnalysisScopeAsync(symbol, isGeneratedCodeSymbol, filterTree, filterSpan, symbolStartActions, analyzerExecutor, cancellationToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                // Task to compute the scope was cancelled.
                // Clear the symbol scope for analyzer, so we can attempt a retry.
                analyzerExecutionContext.ClearSymbolScopeTask(symbol);
 
                cancellationToken.ThrowIfCancellationRequested();
                return await GetSymbolAnalysisScopeCoreAsync(symbol, isGeneratedCodeSymbol, filterTree, filterSpan, symbolStartActions, analyzerExecutor, analyzerExecutionContext, cancellationToken).ConfigureAwait(false);
            }
        }
 
        [PerformanceSensitive("https://github.com/dotnet/roslyn/issues/23582", OftenCompletesSynchronously = true)]
        private async ValueTask<HostSessionStartAnalysisScope> GetSessionAnalysisScopeAsync(
            DiagnosticAnalyzer analyzer,
            AnalyzerExecutor analyzerExecutor,
            CancellationToken cancellationToken)
        {
            var analyzerExecutionContext = GetAnalyzerExecutionContext(analyzer);
            return await GetSessionAnalysisScopeCoreAsync(analyzerExecutor, analyzerExecutionContext, cancellationToken).ConfigureAwait(false);
        }
 
        [PerformanceSensitive("https://github.com/dotnet/roslyn/issues/23582", OftenCompletesSynchronously = true)]
        private async ValueTask<HostSessionStartAnalysisScope> GetSessionAnalysisScopeCoreAsync(
            AnalyzerExecutor analyzerExecutor,
            AnalyzerExecutionContext analyzerExecutionContext,
            CancellationToken cancellationToken)
        {
            try
            {
                var task = analyzerExecutionContext.GetSessionAnalysisScopeAsync(analyzerExecutor, cancellationToken);
                return await task.ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                // Task to compute the scope was cancelled.
                // Clear the entry in scope map for analyzer, so we can attempt a retry.
                analyzerExecutionContext.ClearSessionScopeTask();
 
                cancellationToken.ThrowIfCancellationRequested();
                return await GetSessionAnalysisScopeCoreAsync(analyzerExecutor, analyzerExecutionContext, cancellationToken).ConfigureAwait(false);
            }
        }
 
        /// <summary>
        /// Get all the analyzer actions to execute for the given analyzer against a given compilation.
        /// The returned actions include the actions registered during <see cref="DiagnosticAnalyzer.Initialize(AnalysisContext)"/> method as well as
        /// the actions registered during <see cref="CompilationStartAnalyzerAction"/> for the given compilation.
        /// </summary>
        [PerformanceSensitive("https://github.com/dotnet/roslyn/issues/23582", OftenCompletesSynchronously = true)]
        public async ValueTask<AnalyzerActions> GetAnalyzerActionsAsync(DiagnosticAnalyzer analyzer, AnalyzerExecutor analyzerExecutor, CancellationToken cancellationToken)
        {
            var sessionScope = await GetSessionAnalysisScopeAsync(analyzer, analyzerExecutor, cancellationToken).ConfigureAwait(false);
            if (sessionScope.GetAnalyzerActions().CompilationStartActionsCount > 0 && analyzerExecutor.Compilation != null)
            {
                var compilationScope = await GetCompilationAnalysisScopeAsync(sessionScope, analyzerExecutor, cancellationToken).ConfigureAwait(false);
                return compilationScope.GetAnalyzerActions();
            }
 
            return sessionScope.GetAnalyzerActions();
        }
 
        /// <summary>
        /// Get the per-symbol analyzer actions to be executed by the given analyzer.
        /// These are the actions registered during the various RegisterSymbolStartAction method invocations for the given symbol on different analysis contexts.
        /// </summary>
        [PerformanceSensitive("https://github.com/dotnet/roslyn/issues/23582", OftenCompletesSynchronously = true)]
        public async ValueTask<AnalyzerActions> GetPerSymbolAnalyzerActionsAsync(
            ISymbol symbol,
            bool isGeneratedCodeSymbol,
            SyntaxTree? filterTree,
            TextSpan? filterSpan,
            DiagnosticAnalyzer analyzer,
            AnalyzerExecutor analyzerExecutor,
            CancellationToken cancellationToken)
        {
            var analyzerActions = await GetAnalyzerActionsAsync(analyzer, analyzerExecutor, cancellationToken).ConfigureAwait(false);
            if (analyzerActions.SymbolStartActionsCount > 0)
            {
                var filteredSymbolStartActions = getFilteredActionsByKind(analyzerActions.SymbolStartActions);
                if (filteredSymbolStartActions.Length > 0)
                {
                    var symbolScope = await GetSymbolAnalysisScopeAsync(symbol, isGeneratedCodeSymbol, filterTree, filterSpan, analyzer, filteredSymbolStartActions, analyzerExecutor, cancellationToken).ConfigureAwait(false);
                    return symbolScope.GetAnalyzerActions();
                }
            }
 
            return AnalyzerActions.Empty;
 
            ImmutableArray<SymbolStartAnalyzerAction> getFilteredActionsByKind(ImmutableArray<SymbolStartAnalyzerAction> symbolStartActions)
            {
                ArrayBuilder<SymbolStartAnalyzerAction>? filteredActionsBuilderOpt = null;
                for (int i = 0; i < symbolStartActions.Length; i++)
                {
                    var symbolStartAction = symbolStartActions[i];
                    if (symbolStartAction.Kind != symbol.Kind)
                    {
                        if (filteredActionsBuilderOpt == null)
                        {
                            filteredActionsBuilderOpt = ArrayBuilder<SymbolStartAnalyzerAction>.GetInstance();
                            filteredActionsBuilderOpt.AddRange(symbolStartActions, i);
                        }
                    }
                    else if (filteredActionsBuilderOpt != null)
                    {
                        filteredActionsBuilderOpt.Add(symbolStartAction);
                    }
                }
 
                return filteredActionsBuilderOpt != null ? filteredActionsBuilderOpt.ToImmutableAndFree() : symbolStartActions;
            }
        }
        /// <summary>
        /// Returns true if the given analyzer has enabled concurrent execution by invoking <see cref="AnalysisContext.EnableConcurrentExecution"/>.
        /// </summary>
        public async Task<bool> IsConcurrentAnalyzerAsync(DiagnosticAnalyzer analyzer, AnalyzerExecutor analyzerExecutor, CancellationToken cancellationToken)
        {
            var sessionScope = await GetSessionAnalysisScopeAsync(analyzer, analyzerExecutor, cancellationToken).ConfigureAwait(false);
            return sessionScope.IsConcurrentAnalyzer();
        }
 
        /// <summary>
        /// Returns <see cref="GeneratedCodeAnalysisFlags"/> for the given analyzer.
        /// If an analyzer hasn't configured generated code analysis, returns <see cref="AnalyzerDriver.DefaultGeneratedCodeAnalysisFlags"/>.
        /// </summary>
        public async Task<GeneratedCodeAnalysisFlags> GetGeneratedCodeAnalysisFlagsAsync(DiagnosticAnalyzer analyzer, AnalyzerExecutor analyzerExecutor, CancellationToken cancellationToken)
        {
            var sessionScope = await GetSessionAnalysisScopeAsync(analyzer, analyzerExecutor, cancellationToken).ConfigureAwait(false);
            return sessionScope.GetGeneratedCodeAnalysisFlags();
        }
 
        /// <summary>
        /// Return <see cref="DiagnosticAnalyzer.SupportedDiagnostics"/> of given <paramref name="analyzer"/>.
        /// </summary>
        public ImmutableArray<DiagnosticDescriptor> GetSupportedDiagnosticDescriptors(
            DiagnosticAnalyzer analyzer,
            AnalyzerExecutor analyzerExecutor,
            CancellationToken cancellationToken)
        {
            var analyzerExecutionContext = GetAnalyzerExecutionContext(analyzer);
            return analyzerExecutionContext.GetOrComputeDiagnosticDescriptors(analyzer, analyzerExecutor, cancellationToken);
        }
 
        /// <summary>
        /// Return <see cref="DiagnosticSuppressor.SupportedSuppressions"/> of given <paramref name="suppressor"/>.
        /// </summary>
        public ImmutableArray<SuppressionDescriptor> GetSupportedSuppressionDescriptors(
            DiagnosticSuppressor suppressor,
            AnalyzerExecutor analyzerExecutor,
            CancellationToken cancellationToken)
        {
            var analyzerExecutionContext = GetAnalyzerExecutionContext(suppressor);
            return analyzerExecutionContext.GetOrComputeSuppressionDescriptors(suppressor, analyzerExecutor, cancellationToken);
        }
 
        internal bool IsSupportedDiagnostic(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Func<DiagnosticAnalyzer, bool> isCompilerAnalyzer, AnalyzerExecutor analyzerExecutor, CancellationToken cancellationToken)
        {
            // Avoid realizing all the descriptors for all compiler diagnostics by assuming that compiler analyzer doesn't report unsupported diagnostics.
            if (isCompilerAnalyzer(analyzer))
            {
                return true;
            }
 
            // Get all the supported diagnostics and scan them linearly to see if the reported diagnostic is supported by the analyzer.
            // The linear scan is okay, given that this runs only if a diagnostic is being reported and a given analyzer is quite unlikely to have hundreds of thousands of supported diagnostics.
            var supportedDescriptors = GetSupportedDiagnosticDescriptors(analyzer, analyzerExecutor, cancellationToken);
            foreach (var descriptor in supportedDescriptors)
            {
                if (descriptor.Id.Equals(diagnostic.Id, StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }
            }
 
            return false;
        }
 
        /// <summary>
        /// Returns true if all the diagnostics that can be produced by this analyzer are suppressed through options.
        /// </summary>
        internal bool IsDiagnosticAnalyzerSuppressed(
            DiagnosticAnalyzer analyzer,
            CompilationOptions options,
            Func<DiagnosticAnalyzer, bool> isCompilerAnalyzer,
            AnalyzerExecutor analyzerExecutor,
            AnalysisScope analysisScope,
            SeverityFilter severityFilter,
            CancellationToken cancellationToken)
        {
            Func<DiagnosticAnalyzer, ImmutableArray<DiagnosticDescriptor>> getSupportedDiagnosticDescriptors =
                analyzer => GetSupportedDiagnosticDescriptors(analyzer, analyzerExecutor, cancellationToken);
            Func<DiagnosticSuppressor, ImmutableArray<SuppressionDescriptor>> getSupportedSuppressionDescriptors =
                suppressor => GetSupportedSuppressionDescriptors(suppressor, analyzerExecutor, cancellationToken);
 
            return IsDiagnosticAnalyzerSuppressed(analyzer, options, isCompilerAnalyzer, severityFilter,
                isEnabledWithAnalyzerConfigOptions, getSupportedDiagnosticDescriptors, getSupportedSuppressionDescriptors, cancellationToken);
 
            bool isEnabledWithAnalyzerConfigOptions(DiagnosticDescriptor descriptor)
            {
                if (analyzerExecutor.Compilation.Options.SyntaxTreeOptionsProvider is { } treeOptions)
                {
                    foreach (var tree in analysisScope.SyntaxTrees)
                    {
                        // Check if diagnostic is enabled by SyntaxTree.DiagnosticOptions or Bulk configuration from AnalyzerConfigOptions.
                        if (treeOptions.TryGetDiagnosticValue(tree, descriptor.Id, cancellationToken, out var configuredValue) ||
                            analyzerExecutor.AnalyzerOptions.TryGetSeverityFromBulkConfiguration(tree, analyzerExecutor.Compilation, descriptor, cancellationToken, out configuredValue))
                        {
                            if (configuredValue != ReportDiagnostic.Suppress && !severityFilter.Contains(configuredValue))
                            {
                                return true;
                            }
                        }
                    }
                }
 
                return false;
            }
        }
 
        /// <summary>
        /// Returns true if all the diagnostics that can be produced by this analyzer are suppressed through options.
        /// </summary>
        internal static bool IsDiagnosticAnalyzerSuppressed(
            DiagnosticAnalyzer analyzer,
            CompilationOptions options,
            Func<DiagnosticAnalyzer, bool> isCompilerAnalyzer,
            SeverityFilter severityFilter,
            Func<DiagnosticDescriptor, bool> isEnabledWithAnalyzerConfigOptions,
            Func<DiagnosticAnalyzer, ImmutableArray<DiagnosticDescriptor>> getSupportedDiagnosticDescriptors,
            Func<DiagnosticSuppressor, ImmutableArray<SuppressionDescriptor>> getSupportedSuppressionDescriptors,
            CancellationToken cancellationToken)
        {
            if (isCompilerAnalyzer(analyzer))
            {
                // Compiler analyzer must always be executed for compiler errors, which cannot be suppressed or filtered.
                return false;
            }
 
            var supportedDiagnostics = getSupportedDiagnosticDescriptors(analyzer);
            var diagnosticOptions = options.SpecificDiagnosticOptions;
 
            foreach (var diag in supportedDiagnostics)
            {
                if (diag.IsNotConfigurable())
                {
                    if (diag.IsEnabledByDefault)
                    {
                        // Diagnostic descriptor is not configurable, so the diagnostics created through it cannot be suppressed.
                        return false;
                    }
                    else
                    {
                        // NotConfigurable disabled diagnostic can be ignored as it is never reported.
                        continue;
                    }
                }
                else if (diag.IsCustomSeverityConfigurable())
                {
                    // Analyzer supports custom ways for configuring diagnostic severity that may not be understood by the compiler.
                    // We always consider such analyzers to be non-suppressed. Analyzer is responsible for bailing out early if
                    // it has been suppressed by some custom configuration.
                    return false;
                }
 
                // Is this diagnostic suppressed by default (as written by the rule author)
                var isSuppressed = !diag.IsEnabledByDefault;
 
                // Global editorconfig settings overrides the analyzer author
                // Compilation wide user settings (diagnosticOptions) from ruleset/nowarn/warnaserror overrides the analyzer author and global editorconfig settings.
                // Note that "/warnaserror-:DiagnosticId" adds a diagnostic option with value 'ReportDiagnostic.Default',
                // which should not alter 'isSuppressed'.
                if ((diagnosticOptions.TryGetValue(diag.Id, out var severity) && severity != ReportDiagnostic.Default) ||
                    (options.SyntaxTreeOptionsProvider is object && options.SyntaxTreeOptionsProvider.TryGetGlobalDiagnosticValue(diag.Id, cancellationToken, out severity)))
                {
                    isSuppressed = severity == ReportDiagnostic.Suppress;
                }
                else
                {
                    severity = isSuppressed ? ReportDiagnostic.Suppress : DiagnosticDescriptor.MapSeverityToReport(diag.DefaultSeverity);
                }
 
                // Is this diagnostic suppressed due to its severity
                if (severityFilter.Contains(severity))
                {
                    isSuppressed = true;
                }
 
                // Editorconfig user settings override compilation wide settings.
                if (isSuppressed &&
                    isEnabledWithAnalyzerConfigOptions(diag))
                {
                    isSuppressed = false;
                }
 
                if (!isSuppressed)
                {
                    return false;
                }
            }
 
            if (analyzer is DiagnosticSuppressor suppressor)
            {
                foreach (var suppressionDescriptor in getSupportedSuppressionDescriptors(suppressor))
                {
                    if (!suppressionDescriptor.IsDisabled(options))
                    {
                        return false;
                    }
                }
            }
 
            return true;
        }
 
        internal static bool HasCompilerOrNotConfigurableTagOrCustomConfigurableTag(ImmutableArray<string> customTags)
        {
            foreach (var customTag in customTags)
            {
                if (customTag is WellKnownDiagnosticTags.Compiler or WellKnownDiagnosticTags.NotConfigurable or WellKnownDiagnosticTags.CustomSeverityConfigurable)
                {
                    return true;
                }
            }
 
            return false;
        }
 
        internal static bool HasNotConfigurableTag(ImmutableArray<string> customTags)
            => HasCustomTag(customTags, WellKnownDiagnosticTags.NotConfigurable);
 
        internal static bool HasCustomSeverityConfigurableTag(ImmutableArray<string> customTags)
            => HasCustomTag(customTags, WellKnownDiagnosticTags.CustomSeverityConfigurable);
 
        private static bool HasCustomTag(ImmutableArray<string> customTags, string tagToFind)
        {
            foreach (var customTag in customTags)
            {
                if (customTag == tagToFind)
                {
                    return true;
                }
            }
 
            return false;
        }
 
        public bool TryProcessCompletedMemberAndGetPendingSymbolEndActionsForContainer(
            ISymbol containingSymbol,
            ISymbol processedMemberSymbol,
            DiagnosticAnalyzer analyzer,
            out (ImmutableArray<SymbolEndAnalyzerAction> symbolEndActions, SymbolDeclaredCompilationEvent symbolDeclaredEvent) containerEndActionsAndEvent)
        {
            return GetAnalyzerExecutionContext(analyzer).TryProcessCompletedMemberAndGetPendingSymbolEndActionsForContainer(containingSymbol, processedMemberSymbol, out containerEndActionsAndEvent);
        }
 
        public bool TryStartExecuteSymbolEndActions(ImmutableArray<SymbolEndAnalyzerAction> symbolEndActions, DiagnosticAnalyzer analyzer, SymbolDeclaredCompilationEvent symbolDeclaredEvent)
        {
            return GetAnalyzerExecutionContext(analyzer).TryStartExecuteSymbolEndActions(symbolEndActions, symbolDeclaredEvent);
        }
 
        public void MarkSymbolEndAnalysisPending(
            ISymbol symbol,
            DiagnosticAnalyzer analyzer,
            ImmutableArray<SymbolEndAnalyzerAction> symbolEndActions,
            SymbolDeclaredCompilationEvent symbolDeclaredEvent)
        {
            GetAnalyzerExecutionContext(analyzer).MarkSymbolEndAnalysisPending(symbol, symbolEndActions, symbolDeclaredEvent);
        }
 
        public void MarkSymbolEndAnalysisComplete(ISymbol symbol, DiagnosticAnalyzer analyzer)
        {
            GetAnalyzerExecutionContext(analyzer).MarkSymbolEndAnalysisComplete(symbol);
        }
 
        [Conditional("DEBUG")]
        public void VerifyAllSymbolEndActionsExecuted()
        {
            foreach (var analyzerExecutionContext in _analyzerExecutionContextMap.Values)
            {
                analyzerExecutionContext.VerifyAllSymbolEndActionsExecuted();
            }
        }
    }
}