|
// 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.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.FlowAnalysis;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Diagnostics
{
/// <summary>
/// Contains the core execution logic for callbacks into analyzers.
/// </summary>
internal partial class AnalyzerExecutor
{
private const string DiagnosticCategory = "Compiler";
// internal for testing purposes only.
internal const string AnalyzerExceptionDiagnosticId = "AD0001";
internal const string AnalyzerDriverExceptionDiagnosticId = "AD0002";
private readonly Action<Diagnostic, CancellationToken>? _addNonCategorizedDiagnostic;
private readonly Action<Diagnostic, DiagnosticAnalyzer, bool, CancellationToken>? _addCategorizedLocalDiagnostic;
private readonly Action<Diagnostic, DiagnosticAnalyzer, CancellationToken>? _addCategorizedNonLocalDiagnostic;
private readonly Action<Suppression>? _addSuppression;
private readonly Func<Exception, bool>? _analyzerExceptionFilter;
private readonly AnalyzerManager _analyzerManager;
private readonly Func<DiagnosticAnalyzer, bool> _isCompilerAnalyzer;
private readonly Func<DiagnosticAnalyzer, object?> _getAnalyzerGate;
private readonly Func<SyntaxTree, SemanticModel> _getSemanticModel;
private readonly Func<DiagnosticAnalyzer, bool> _shouldSkipAnalysisOnGeneratedCode;
private readonly Func<Diagnostic, DiagnosticAnalyzer, Compilation, CancellationToken, bool> _shouldSuppressGeneratedCodeDiagnostic;
private readonly Func<SyntaxTree, TextSpan, CancellationToken, bool> _isGeneratedCodeLocation;
private readonly Func<DiagnosticAnalyzer, SyntaxTree, SyntaxTreeOptionsProvider?, CancellationToken, bool> _isAnalyzerSuppressedForTree;
/// <summary>
/// The values in this map convert to <see cref="TimeSpan"/> using <see cref="TimeSpan.FromTicks(long)"/>.
/// </summary>
private readonly ConcurrentDictionary<DiagnosticAnalyzer, StrongBox<long>>? _analyzerExecutionTimeMap;
private readonly CompilationAnalysisValueProviderFactory _compilationAnalysisValueProviderFactory;
private Func<IOperation, ControlFlowGraph>? _lazyGetControlFlowGraph;
private ConcurrentDictionary<IOperation, ControlFlowGraph>? _lazyControlFlowGraphMap;
private Func<IOperation, ControlFlowGraph> GetControlFlowGraph
=> _lazyGetControlFlowGraph ??= GetControlFlowGraphImpl;
private bool IsAnalyzerSuppressedForTree(DiagnosticAnalyzer analyzer, SyntaxTree tree, CancellationToken cancellationToken)
{
return _isAnalyzerSuppressedForTree(analyzer, tree, Compilation.Options.SyntaxTreeOptionsProvider, cancellationToken);
}
/// <summary>
/// Creates <see cref="AnalyzerExecutor"/> to execute analyzer actions with given arguments
/// </summary>
/// <param name="compilation">Compilation to be used in the analysis.</param>
/// <param name="analyzerOptions">Analyzer options.</param>
/// <param name="addNonCategorizedDiagnostic">Optional delegate to add non-categorized analyzer diagnostics.</param>
/// <param name="onAnalyzerException">
/// Delegate which is invoked when an analyzer throws an exception.
/// Delegate can do custom tasks such as report the given analyzer exception diagnostic, report a non-fatal watson for the exception, etc.
/// </param>
/// <param name="analyzerExceptionFilter">
/// Optional delegate which is invoked when an analyzer throws an exception as an exception filter.
/// Delegate can do custom tasks such as crash hosting process to create a dump.
/// </param>
/// <param name="isCompilerAnalyzer">Delegate to determine if the given analyzer is compiler analyzer.
/// We need to special case the compiler analyzer at few places for performance reasons.</param>
/// <param name="analyzerManager">Analyzer manager to fetch supported diagnostics.</param>
/// <param name="getAnalyzerGate">
/// Delegate to fetch the gate object to guard all callbacks into the analyzer.
/// It should return a unique gate object for the given analyzer instance for non-concurrent analyzers, and null otherwise.
/// All analyzer callbacks for non-concurrent analyzers will be guarded with a lock on the gate.
/// </param>
/// <param name="getSemanticModel">Delegate to get a semantic model for the given syntax tree which can be shared across analyzers.</param>
/// <param name="severityFilter"><see cref="SeverityFilter"/> for analysis.</param>
/// <param name="shouldSkipAnalysisOnGeneratedCode">Delegate to identify if analysis should be skipped on generated code.</param>
/// <param name="shouldSuppressGeneratedCodeDiagnostic">Delegate to identify if diagnostic reported while analyzing generated code should be suppressed.</param>
/// <param name="isGeneratedCodeLocation">Delegate to identify if the given location is in generated code.</param>
/// <param name="isAnalyzerSuppressedForTree">Delegate to identify if the given analyzer is suppressed for the given tree.</param>
/// <param name="logExecutionTime">Flag indicating whether we need to log analyzer execution time.</param>
/// <param name="addCategorizedLocalDiagnostic">Optional delegate to add categorized local analyzer diagnostics.</param>
/// <param name="addCategorizedNonLocalDiagnostic">Optional delegate to add categorized non-local analyzer diagnostics.</param>
/// <param name="addSuppression">Optional thread-safe delegate to add diagnostic suppressions from suppressors.</param>
public static AnalyzerExecutor Create(
Compilation compilation,
AnalyzerOptions analyzerOptions,
Action<Diagnostic, CancellationToken>? addNonCategorizedDiagnostic,
Action<Exception, DiagnosticAnalyzer, Diagnostic, CancellationToken> onAnalyzerException,
Func<Exception, bool>? analyzerExceptionFilter,
Func<DiagnosticAnalyzer, bool> isCompilerAnalyzer,
AnalyzerManager analyzerManager,
Func<DiagnosticAnalyzer, bool> shouldSkipAnalysisOnGeneratedCode,
Func<Diagnostic, DiagnosticAnalyzer, Compilation, CancellationToken, bool> shouldSuppressGeneratedCodeDiagnostic,
Func<SyntaxTree, TextSpan, CancellationToken, bool> isGeneratedCodeLocation,
Func<DiagnosticAnalyzer, SyntaxTree, SyntaxTreeOptionsProvider?, CancellationToken, bool> isAnalyzerSuppressedForTree,
Func<DiagnosticAnalyzer, object?> getAnalyzerGate,
Func<SyntaxTree, SemanticModel> getSemanticModel,
SeverityFilter severityFilter,
bool logExecutionTime = false,
Action<Diagnostic, DiagnosticAnalyzer, bool, CancellationToken>? addCategorizedLocalDiagnostic = null,
Action<Diagnostic, DiagnosticAnalyzer, CancellationToken>? addCategorizedNonLocalDiagnostic = null,
Action<Suppression>? addSuppression = null)
{
// We can either report categorized (local/non-local) diagnostics or non-categorized diagnostics.
Debug.Assert((addNonCategorizedDiagnostic != null) ^ (addCategorizedLocalDiagnostic != null));
Debug.Assert((addCategorizedLocalDiagnostic != null) == (addCategorizedNonLocalDiagnostic != null));
var analyzerExecutionTimeMap = logExecutionTime ? new ConcurrentDictionary<DiagnosticAnalyzer, StrongBox<long>>() : null;
return new AnalyzerExecutor(compilation, analyzerOptions, addNonCategorizedDiagnostic, onAnalyzerException, analyzerExceptionFilter,
isCompilerAnalyzer, analyzerManager, shouldSkipAnalysisOnGeneratedCode, shouldSuppressGeneratedCodeDiagnostic, isGeneratedCodeLocation,
isAnalyzerSuppressedForTree, getAnalyzerGate, getSemanticModel, severityFilter, analyzerExecutionTimeMap, addCategorizedLocalDiagnostic, addCategorizedNonLocalDiagnostic,
addSuppression);
}
private AnalyzerExecutor(
Compilation compilation,
AnalyzerOptions analyzerOptions,
Action<Diagnostic, CancellationToken>? addNonCategorizedDiagnosticOpt,
Action<Exception, DiagnosticAnalyzer, Diagnostic, CancellationToken> onAnalyzerException,
Func<Exception, bool>? analyzerExceptionFilter,
Func<DiagnosticAnalyzer, bool> isCompilerAnalyzer,
AnalyzerManager analyzerManager,
Func<DiagnosticAnalyzer, bool> shouldSkipAnalysisOnGeneratedCode,
Func<Diagnostic, DiagnosticAnalyzer, Compilation, CancellationToken, bool> shouldSuppressGeneratedCodeDiagnostic,
Func<SyntaxTree, TextSpan, CancellationToken, bool> isGeneratedCodeLocation,
Func<DiagnosticAnalyzer, SyntaxTree, SyntaxTreeOptionsProvider?, CancellationToken, bool> isAnalyzerSuppressedForTree,
Func<DiagnosticAnalyzer, object?> getAnalyzerGate,
Func<SyntaxTree, SemanticModel> getSemanticModel,
SeverityFilter severityFilter,
ConcurrentDictionary<DiagnosticAnalyzer, StrongBox<long>>? analyzerExecutionTimeMap,
Action<Diagnostic, DiagnosticAnalyzer, bool, CancellationToken>? addCategorizedLocalDiagnostic,
Action<Diagnostic, DiagnosticAnalyzer, CancellationToken>? addCategorizedNonLocalDiagnostic,
Action<Suppression>? addSuppression)
{
Compilation = compilation;
AnalyzerOptions = analyzerOptions;
_addNonCategorizedDiagnostic = addNonCategorizedDiagnosticOpt;
OnAnalyzerException = onAnalyzerException;
_analyzerExceptionFilter = analyzerExceptionFilter;
_isCompilerAnalyzer = isCompilerAnalyzer;
_analyzerManager = analyzerManager;
_shouldSkipAnalysisOnGeneratedCode = shouldSkipAnalysisOnGeneratedCode;
_shouldSuppressGeneratedCodeDiagnostic = shouldSuppressGeneratedCodeDiagnostic;
_isGeneratedCodeLocation = isGeneratedCodeLocation;
_isAnalyzerSuppressedForTree = isAnalyzerSuppressedForTree;
_getAnalyzerGate = getAnalyzerGate;
_getSemanticModel = getSemanticModel;
SeverityFilter = severityFilter;
_analyzerExecutionTimeMap = analyzerExecutionTimeMap;
_addCategorizedLocalDiagnostic = addCategorizedLocalDiagnostic;
_addCategorizedNonLocalDiagnostic = addCategorizedNonLocalDiagnostic;
_addSuppression = addSuppression;
_compilationAnalysisValueProviderFactory = new CompilationAnalysisValueProviderFactory();
}
internal Compilation Compilation { get; }
internal AnalyzerOptions AnalyzerOptions { get; }
internal SeverityFilter SeverityFilter { get; }
internal Action<Exception, DiagnosticAnalyzer, Diagnostic, CancellationToken> OnAnalyzerException { get; }
internal ImmutableDictionary<DiagnosticAnalyzer, TimeSpan> AnalyzerExecutionTimes
{
get
{
Debug.Assert(_analyzerExecutionTimeMap != null);
return _analyzerExecutionTimeMap.ToImmutableDictionary(pair => pair.Key, pair => TimeSpan.FromTicks(pair.Value.Value));
}
}
/// <summary>
/// Executes the <see cref="DiagnosticAnalyzer.Initialize(AnalysisContext)"/> for the given analyzer.
/// </summary>
/// <param name="sessionScope">Session scope to store register session wide analyzer actions.</param>
/// <param name="severityFilter">Severity filter for analysis.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <remarks>
/// Note that this API doesn't execute any <see cref="CompilationStartAnalyzerAction"/> registered by the Initialize invocation.
/// Use <see cref="ExecuteCompilationStartActions(ImmutableArray{CompilationStartAnalyzerAction}, HostCompilationStartAnalysisScope, CancellationToken)"/> API
/// to get execute these actions to get the per-compilation analyzer actions.
/// </remarks>
public void ExecuteInitializeMethod(HostSessionStartAnalysisScope sessionScope, SeverityFilter severityFilter, CancellationToken cancellationToken)
{
var context = new AnalyzerAnalysisContext(sessionScope, severityFilter);
ExecuteAndCatchIfThrows(
sessionScope.Analyzer,
static data => data.sessionScope.Analyzer.Initialize(data.context),
(sessionScope, context),
contextInfo: null,
cancellationToken);
}
/// <summary>
/// Executes the compilation start actions.
/// </summary>
/// <param name="actions"><see cref="AnalyzerActions"/> whose compilation start actions are to be executed.</param>
/// <param name="compilationScope">Compilation scope to store the analyzer actions.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public void ExecuteCompilationStartActions(ImmutableArray<CompilationStartAnalyzerAction> actions, HostCompilationStartAnalysisScope compilationScope, CancellationToken cancellationToken)
{
// This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we
// can use the same instance across all actions.
var context = new AnalyzerCompilationStartAnalysisContext(compilationScope,
Compilation, AnalyzerOptions, _compilationAnalysisValueProviderFactory, cancellationToken);
var contextInfo = new AnalysisContextInfo(Compilation);
foreach (var startAction in actions)
{
ExecuteAndCatchIfThrows(
startAction.Analyzer,
static data => data.startAction.Action(data.context),
(startAction, context),
contextInfo,
cancellationToken);
}
}
/// <summary>
/// Executes the symbol start actions.
/// </summary>
/// <param name="symbol">Symbol whose symbol start actions are to be executed.</param>
/// <param name="actions"><see cref="AnalyzerActions"/> whose symbol start actions are to be executed.</param>
/// <param name="symbolScope">Symbol scope to store the analyzer actions.</param>
/// <param name="isGeneratedCodeSymbol">Flag indicating if the symbol being analyzed is generated code.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public void ExecuteSymbolStartActions(
ISymbol symbol,
ImmutableArray<SymbolStartAnalyzerAction> actions,
HostSymbolStartAnalysisScope symbolScope,
bool isGeneratedCodeSymbol,
SyntaxTree? filterTree,
TextSpan? filterSpan,
CancellationToken cancellationToken)
{
if (isGeneratedCodeSymbol && _shouldSkipAnalysisOnGeneratedCode(symbolScope.Analyzer) ||
IsAnalyzerSuppressedForSymbol(symbolScope.Analyzer, symbol, cancellationToken))
{
return;
}
// This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we
// can use the same instance across all actions.
var context = new AnalyzerSymbolStartAnalysisContext(symbolScope,
symbol, Compilation, AnalyzerOptions, isGeneratedCodeSymbol, filterTree, filterSpan, cancellationToken);
var contextInfo = new AnalysisContextInfo(Compilation, symbol);
foreach (var startAction in actions)
{
Debug.Assert(startAction.Analyzer == symbolScope.Analyzer);
ExecuteAndCatchIfThrows(
startAction.Analyzer,
static data => data.startAction.Action(data.context),
(startAction, context),
contextInfo,
cancellationToken);
}
}
/// <summary>
/// Executes the given diagnostic suppressor.
/// </summary>
/// <param name="suppressor">Suppressor to be executed.</param>
/// <param name="reportedDiagnostics">Reported analyzer/compiler diagnostics that can be suppressed.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public void ExecuteSuppressionAction(DiagnosticSuppressor suppressor, ImmutableArray<Diagnostic> reportedDiagnostics, CancellationToken cancellationToken)
{
Debug.Assert(_addSuppression != null);
if (reportedDiagnostics.IsEmpty)
{
return;
}
cancellationToken.ThrowIfCancellationRequested();
var supportedSuppressions = _analyzerManager.GetSupportedSuppressionDescriptors(suppressor, this, cancellationToken);
using var _ = PooledDelegates.GetPooledFunction(
static (d, supportedSuppressions) => supportedSuppressions.Contains(d),
supportedSuppressions,
out Func<SuppressionDescriptor, bool> isSupportedSuppression);
var context = new SuppressionAnalysisContext(Compilation, AnalyzerOptions,
reportedDiagnostics, _addSuppression, isSupportedSuppression, _getSemanticModel, cancellationToken);
ExecuteAndCatchIfThrows(
suppressor,
static data => data.suppressor.ReportSuppressions(data.context),
(suppressor, context),
new AnalysisContextInfo(Compilation),
cancellationToken);
}
/// <summary>
/// Executes compilation actions or compilation end actions.
/// </summary>
/// <param name="compilationActions">Compilation actions to be executed.</param>
/// <param name="analyzer">Analyzer whose actions are to be executed.</param>
/// <param name="compilationEvent">Compilation event.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public void ExecuteCompilationActions(
ImmutableArray<CompilationAnalyzerAction> compilationActions,
DiagnosticAnalyzer analyzer,
CompilationEvent compilationEvent,
CancellationToken cancellationToken)
{
Debug.Assert(compilationEvent is CompilationStartedEvent || compilationEvent is CompilationCompletedEvent);
var addDiagnostic = GetAddCompilationDiagnostic(analyzer, cancellationToken);
using var _ = PooledDelegates.GetPooledFunction(
static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct),
(self: this, analyzer),
out Func<Diagnostic, CancellationToken, bool> isSupportedDiagnostic);
// This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we
// can use the same instance across all actions.
var context = new CompilationAnalysisContext(
Compilation, AnalyzerOptions, addDiagnostic,
isSupportedDiagnostic, _compilationAnalysisValueProviderFactory, cancellationToken);
var contextInfo = new AnalysisContextInfo(Compilation);
foreach (var endAction in compilationActions)
{
ExecuteAndCatchIfThrows(
endAction.Analyzer,
static data => data.endAction.Action(data.context),
(endAction, context),
contextInfo,
cancellationToken);
}
}
/// <summary>
/// Execute the symbol actions on the given symbol.
/// </summary>
/// <param name="symbolActions">Symbol actions to be executed.</param>
/// <param name="analyzer">Analyzer whose actions are to be executed.</param>
/// <param name="symbolDeclaredEvent">Symbol event to be analyzed.</param>
/// <param name="getTopMostNodeForAnalysis">Delegate to get topmost declaration node for a symbol declaration reference.</param>
/// <param name="isGeneratedCodeSymbol">Flag indicating if this is a generated code symbol.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public void ExecuteSymbolActions(
ImmutableArray<SymbolAnalyzerAction> symbolActions,
DiagnosticAnalyzer analyzer,
SymbolDeclaredCompilationEvent symbolDeclaredEvent,
Func<ISymbol, SyntaxReference, Compilation, CancellationToken, SyntaxNode> getTopMostNodeForAnalysis,
bool isGeneratedCodeSymbol,
SyntaxTree? filterTree,
TextSpan? filterSpan,
CancellationToken cancellationToken)
{
Debug.Assert(getTopMostNodeForAnalysis != null);
Debug.Assert(!filterSpan.HasValue || filterTree != null);
if (isGeneratedCodeSymbol && _shouldSkipAnalysisOnGeneratedCode(analyzer) ||
IsAnalyzerSuppressedForSymbol(analyzer, symbolDeclaredEvent.Symbol, cancellationToken))
{
return;
}
var symbol = symbolDeclaredEvent.Symbol;
var addDiagnostic = GetAddDiagnostic(symbol, symbolDeclaredEvent.DeclaringSyntaxReferences, analyzer, getTopMostNodeForAnalysis, cancellationToken);
using var _ = PooledDelegates.GetPooledFunction(
static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct),
(self: this, analyzer),
out Func<Diagnostic, CancellationToken, bool> isSupportedDiagnostic);
// This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we
// can use the same instance across all actions.
var context = new SymbolAnalysisContext(
symbol, Compilation, AnalyzerOptions, addDiagnostic,
isSupportedDiagnostic, isGeneratedCodeSymbol, filterTree,
filterSpan, cancellationToken);
var contextInfo = new AnalysisContextInfo(Compilation, symbol);
foreach (var symbolAction in symbolActions)
{
cancellationToken.ThrowIfCancellationRequested();
if (symbolAction.Kinds.Contains(symbol.Kind))
{
ExecuteAndCatchIfThrows(
symbolAction.Analyzer,
static data => data.symbolAction.Action(data.context),
(symbolAction, context),
contextInfo,
cancellationToken);
}
}
}
/// <summary>
/// Execute the symbol end actions on the given namespace or type containing symbol for the process member symbol for the given analyzer.
/// </summary>
/// <param name="containingSymbol">Symbol whose actions are to be executed.</param>
/// <param name="processedMemberSymbol">Completed member symbol.</param>
/// <param name="analyzer">Analyzer whose actions are to be executed.</param>
/// <param name="getTopMostNodeForAnalysis">Delegate to get topmost declaration node for a symbol declaration reference.</param>
/// <param name="filterSpan">Optional filter span for analysis.</param>
/// <param name="isGeneratedCode">Flag indicating if the containing symbol being analyzed is generated code.</param>
public bool TryExecuteSymbolEndActionsForContainer(
INamespaceOrTypeSymbol containingSymbol,
ISymbol processedMemberSymbol,
DiagnosticAnalyzer analyzer,
Func<ISymbol, SyntaxReference, Compilation, CancellationToken, SyntaxNode> getTopMostNodeForAnalysis,
bool isGeneratedCode,
SyntaxTree? filterTree,
TextSpan? filterSpan,
CancellationToken cancellationToken,
[NotNullWhen(returnValue: true)] out SymbolDeclaredCompilationEvent? containingSymbolDeclaredEvent)
{
containingSymbolDeclaredEvent = null;
if (!_analyzerManager.TryProcessCompletedMemberAndGetPendingSymbolEndActionsForContainer(containingSymbol, processedMemberSymbol, analyzer, out var containerEndActionsAndEvent))
{
return false;
}
ImmutableArray<SymbolEndAnalyzerAction> endActions = containerEndActionsAndEvent.symbolEndActions;
containingSymbolDeclaredEvent = containerEndActionsAndEvent.symbolDeclaredEvent;
ExecuteSymbolEndActionsCore(endActions, analyzer, containingSymbolDeclaredEvent, getTopMostNodeForAnalysis, isGeneratedCode, filterTree, filterSpan, cancellationToken);
return true;
}
/// <summary>
/// Tries to execute the symbol end actions on the given symbol for the given analyzer.
/// </summary>
/// <param name="symbolEndActions">Symbol actions to be executed.</param>
/// <param name="analyzer">Analyzer whose actions are to be executed.</param>
/// <param name="symbolDeclaredEvent">Symbol event to be analyzed.</param>
/// <param name="getTopMostNodeForAnalysis">Delegate to get topmost declaration node for a symbol declaration reference.</param>
/// <param name="filterSpan">Optional filter span for analysis.</param>
/// <param name="isGeneratedCode">Flag indicating if the symbol being analyzed is generated code.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>
/// True, if successfully executed the actions for the given analysis scope OR all the actions have already been executed for the given analysis scope.
/// False, if there are some pending actions.
/// </returns>
public bool TryExecuteSymbolEndActions(
ImmutableArray<SymbolEndAnalyzerAction> symbolEndActions,
DiagnosticAnalyzer analyzer,
SymbolDeclaredCompilationEvent symbolDeclaredEvent,
Func<ISymbol, SyntaxReference, Compilation, CancellationToken, SyntaxNode> getTopMostNodeForAnalysis,
bool isGeneratedCode,
SyntaxTree? filterTree,
TextSpan? filterSpan,
CancellationToken cancellationToken)
{
if (!_analyzerManager.TryStartExecuteSymbolEndActions(symbolEndActions, analyzer, symbolDeclaredEvent))
return false;
ExecuteSymbolEndActionsCore(symbolEndActions, analyzer, symbolDeclaredEvent, getTopMostNodeForAnalysis, isGeneratedCode, filterTree, filterSpan, cancellationToken);
return true;
}
private void ExecuteSymbolEndActionsCore(
ImmutableArray<SymbolEndAnalyzerAction> symbolEndActions,
DiagnosticAnalyzer analyzer,
SymbolDeclaredCompilationEvent symbolDeclaredEvent,
Func<ISymbol, SyntaxReference, Compilation, CancellationToken, SyntaxNode> getTopMostNodeForAnalysis,
bool isGeneratedCode,
SyntaxTree? filterTree,
TextSpan? filterSpan,
CancellationToken cancellationToken)
{
Debug.Assert(getTopMostNodeForAnalysis != null);
Debug.Assert(!isGeneratedCode || !_shouldSkipAnalysisOnGeneratedCode(analyzer));
Debug.Assert(!IsAnalyzerSuppressedForSymbol(analyzer, symbolDeclaredEvent.Symbol, cancellationToken));
Debug.Assert(!filterSpan.HasValue || filterTree != null);
var symbol = symbolDeclaredEvent.Symbol;
var addDiagnostic = GetAddDiagnostic(symbol, symbolDeclaredEvent.DeclaringSyntaxReferences, analyzer, getTopMostNodeForAnalysis, cancellationToken);
using var _ = PooledDelegates.GetPooledFunction(
static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct),
(self: this, analyzer),
out Func<Diagnostic, CancellationToken, bool> isSupportedDiagnostic);
// This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we
// can use the same instance across all actions.
var context = new SymbolAnalysisContext(symbol, Compilation, AnalyzerOptions, addDiagnostic,
isSupportedDiagnostic, isGeneratedCode, filterTree, filterSpan, cancellationToken);
var contextInfo = new AnalysisContextInfo(Compilation, symbol);
foreach (var symbolAction in symbolEndActions)
{
ExecuteAndCatchIfThrows(
symbolAction.Analyzer,
static data => data.symbolAction.Action(data.context),
(symbolAction, context),
contextInfo,
cancellationToken);
}
_analyzerManager.MarkSymbolEndAnalysisComplete(symbol, analyzer);
}
/// <summary>
/// Execute the semantic model actions on the given semantic model.
/// </summary>
/// <param name="semanticModelActions">Semantic model actions to be executed.</param>
/// <param name="analyzer">Analyzer whose actions are to be executed.</param>
/// <param name="semanticModel">Semantic model to analyze.</param>
/// <param name="filterSpan">Optional filter span for analysis.</param>
/// <param name="isGeneratedCode">Flag indicating if the syntax tree being analyzed is generated code.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public void ExecuteSemanticModelActions(
ImmutableArray<SemanticModelAnalyzerAction> semanticModelActions,
DiagnosticAnalyzer analyzer,
SemanticModel semanticModel,
TextSpan? filterSpan,
bool isGeneratedCode,
CancellationToken cancellationToken)
{
if (isGeneratedCode && _shouldSkipAnalysisOnGeneratedCode(analyzer) ||
IsAnalyzerSuppressedForTree(analyzer, semanticModel.SyntaxTree, cancellationToken))
{
return;
}
var diagReporter = GetAddSemanticDiagnostic(semanticModel.SyntaxTree, analyzer, cancellationToken);
using var _ = PooledDelegates.GetPooledFunction(
static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct),
(self: this, analyzer),
out Func<Diagnostic, CancellationToken, bool> isSupportedDiagnostic);
// This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we
// can use the same instance across all actions.
var context = new SemanticModelAnalysisContext(
semanticModel, AnalyzerOptions, diagReporter.AddDiagnosticAction,
isSupportedDiagnostic, filterSpan, isGeneratedCode, cancellationToken);
var contextInfo = new AnalysisContextInfo(semanticModel);
foreach (var semanticModelAction in semanticModelActions)
{
ExecuteAndCatchIfThrows(
semanticModelAction.Analyzer,
static data => data.semanticModelAction.Action(data.context),
(semanticModelAction, context),
contextInfo,
cancellationToken);
}
diagReporter.Free();
}
/// <summary>
/// Execute the syntax tree actions on the given syntax tree.
/// </summary>
/// <param name="syntaxTreeActions">Syntax tree actions to be executed.</param>
/// <param name="analyzer">Analyzer whose actions are to be executed.</param>
/// <param name="file">Syntax tree to analyze.</param>
/// <param name="filterSpan">Optional filter span within the <paramref name="file"/> for analysis.</param>
/// <param name="isGeneratedCode">Flag indicating if the syntax tree being analyzed is generated code.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public void ExecuteSyntaxTreeActions(
ImmutableArray<SyntaxTreeAnalyzerAction> syntaxTreeActions,
DiagnosticAnalyzer analyzer,
SourceOrAdditionalFile file,
TextSpan? filterSpan,
bool isGeneratedCode,
CancellationToken cancellationToken)
{
Debug.Assert(file.SourceTree != null);
var tree = file.SourceTree;
if (isGeneratedCode && _shouldSkipAnalysisOnGeneratedCode(analyzer) ||
IsAnalyzerSuppressedForTree(analyzer, tree, cancellationToken))
{
return;
}
var diagReporter = GetAddSyntaxDiagnostic(file, analyzer, cancellationToken);
using var _ = PooledDelegates.GetPooledFunction(
static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct),
(self: this, analyzer),
out Func<Diagnostic, CancellationToken, bool> isSupportedDiagnostic);
// This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we
// can use the same instance across all actions.
var context = new SyntaxTreeAnalysisContext(
tree, AnalyzerOptions, diagReporter.AddDiagnosticAction, isSupportedDiagnostic,
Compilation, filterSpan, isGeneratedCode, cancellationToken);
var contextInfo = new AnalysisContextInfo(Compilation, file);
foreach (var syntaxTreeAction in syntaxTreeActions)
{
ExecuteAndCatchIfThrows(
syntaxTreeAction.Analyzer,
static data => data.syntaxTreeAction.Action(data.context),
(syntaxTreeAction, context),
contextInfo,
cancellationToken);
}
diagReporter.Free();
}
/// <summary>
/// Execute the additional file actions.
/// </summary>
/// <param name="additionalFileActions">Actions to be executed.</param>
/// <param name="analyzer">Analyzer whose actions are to be executed.</param>
/// <param name="file">Additional file to analyze.</param>
/// <param name="filterSpan">Optional filter span within the <paramref name="file"/> for analysis.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public void ExecuteAdditionalFileActions(
ImmutableArray<AdditionalFileAnalyzerAction> additionalFileActions,
DiagnosticAnalyzer analyzer,
SourceOrAdditionalFile file,
TextSpan? filterSpan,
CancellationToken cancellationToken)
{
Debug.Assert(file.AdditionalFile != null);
var additionalFile = file.AdditionalFile;
var diagReporter = GetAddSyntaxDiagnostic(file, analyzer, cancellationToken);
using var _ = PooledDelegates.GetPooledFunction(
static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct),
(self: this, analyzer),
out Func<Diagnostic, CancellationToken, bool> isSupportedDiagnostic);
// This context doesn't build up any state as we pass it to the Action method of the analyzer. As such, we
// can use the same instance across all actions.
var context = new AdditionalFileAnalysisContext(
additionalFile, AnalyzerOptions, diagReporter.AddDiagnosticAction, isSupportedDiagnostic,
Compilation, filterSpan, cancellationToken);
var contextInfo = new AnalysisContextInfo(Compilation, file);
foreach (var additionalFileAction in additionalFileActions)
{
ExecuteAndCatchIfThrows(
additionalFileAction.Analyzer,
static data => data.additionalFileAction.Action(data.context),
(additionalFileAction, context),
contextInfo,
cancellationToken);
}
diagReporter.Free();
}
private void ExecuteSyntaxNodeAction<TLanguageKindEnum>(
SyntaxNodeAnalyzerAction<TLanguageKindEnum> syntaxNodeAction,
SyntaxNode node,
ExecutionData executionData,
Action<Diagnostic> addDiagnostic,
Func<Diagnostic, CancellationToken, bool> isSupportedDiagnostic,
CancellationToken cancellationToken)
where TLanguageKindEnum : struct
{
Debug.Assert(!executionData.IsGeneratedCode || !_shouldSkipAnalysisOnGeneratedCode(syntaxNodeAction.Analyzer));
Debug.Assert(!IsAnalyzerSuppressedForTree(syntaxNodeAction.Analyzer, node.SyntaxTree, cancellationToken));
var syntaxNodeContext = new SyntaxNodeAnalysisContext(
node, executionData.DeclaredSymbol, executionData.SemanticModel, AnalyzerOptions, addDiagnostic,
isSupportedDiagnostic, executionData.FilterSpan, executionData.IsGeneratedCode, cancellationToken);
ExecuteAndCatchIfThrows(
syntaxNodeAction.Analyzer,
static data => data.syntaxNodeAction.Action(data.syntaxNodeContext),
(syntaxNodeAction, syntaxNodeContext),
new AnalysisContextInfo(Compilation, node),
cancellationToken);
}
private void ExecuteOperationAction(
OperationAnalyzerAction operationAction,
IOperation operation,
ExecutionData executionData,
Action<Diagnostic> addDiagnostic,
Func<Diagnostic, CancellationToken, bool> isSupportedDiagnostic,
CancellationToken cancellationToken)
{
Debug.Assert(!executionData.IsGeneratedCode || !_shouldSkipAnalysisOnGeneratedCode(operationAction.Analyzer));
Debug.Assert(!IsAnalyzerSuppressedForTree(operationAction.Analyzer, executionData.SemanticModel.SyntaxTree, cancellationToken));
var operationContext = new OperationAnalysisContext(
operation, executionData.DeclaredSymbol, executionData.SemanticModel.Compilation,
AnalyzerOptions, addDiagnostic, isSupportedDiagnostic, GetControlFlowGraph,
executionData.FilterSpan, executionData.IsGeneratedCode, cancellationToken);
ExecuteAndCatchIfThrows(
operationAction.Analyzer,
static data => data.operationAction.Action(data.operationContext),
(operationAction, operationContext),
new AnalysisContextInfo(Compilation, operation),
cancellationToken);
}
private readonly struct ExecutionData(
DiagnosticAnalyzer analyzer,
ISymbol declaredSymbol,
SemanticModel semanticModel,
TextSpan? filterSpan,
bool isGeneratedCode)
{
public readonly DiagnosticAnalyzer Analyzer = analyzer;
public readonly ISymbol DeclaredSymbol = declaredSymbol;
public readonly SemanticModel SemanticModel = semanticModel;
public readonly TextSpan? FilterSpan = filterSpan;
public readonly bool IsGeneratedCode = isGeneratedCode;
}
/// <summary>
/// Execute code block actions for the given analyzer for the given declaration.
/// </summary>
public void ExecuteCodeBlockActions<TLanguageKindEnum>(
ImmutableArray<CodeBlockStartAnalyzerAction<TLanguageKindEnum>> startActions,
ImmutableArray<CodeBlockAnalyzerAction> actions,
ImmutableArray<CodeBlockAnalyzerAction> endActions,
DiagnosticAnalyzer analyzer,
SyntaxNode declaredNode,
ISymbol declaredSymbol,
ImmutableArray<SyntaxNode> executableCodeBlocks,
SemanticModel semanticModel,
Func<SyntaxNode, TLanguageKindEnum> getKind,
TextSpan? filterSpan,
bool isGeneratedCode,
CancellationToken cancellationToken)
where TLanguageKindEnum : struct
{
Debug.Assert(!executableCodeBlocks.IsEmpty);
// The actions we discover in 'addActions' and then execute in 'executeActions'.
var ephemeralActions = ArrayBuilder<SyntaxNodeAnalyzerAction<TLanguageKindEnum>>.GetInstance();
ExecuteBlockActionsCore(
startActions,
actions,
endActions,
declaredNode,
new ExecutionData(analyzer, declaredSymbol, semanticModel, filterSpan, isGeneratedCode),
addActions: static (startAction, endActions, executionData, args, cancellationToken) =>
{
var (@this, startActions, executableCodeBlocks, declaredNode, getKind, ephemeralActions) = args;
var scope = new HostCodeBlockStartAnalysisScope<TLanguageKindEnum>(startAction.Analyzer);
var startContext = new AnalyzerCodeBlockStartAnalysisContext<TLanguageKindEnum>(
scope, declaredNode, executionData.DeclaredSymbol, executionData.SemanticModel,
@this.AnalyzerOptions, executionData.FilterSpan, executionData.IsGeneratedCode, cancellationToken);
// Catch Exception from the start action.
@this.ExecuteAndCatchIfThrows(
startAction.Analyzer,
static args => args.startAction.Action(args.startContext),
argument: (startAction, startContext),
new AnalysisContextInfo(@this.Compilation, executionData.DeclaredSymbol, declaredNode),
cancellationToken);
endActions.AddAll(scope.CodeBlockEndActions);
ephemeralActions.AddRange(scope.SyntaxNodeActions);
},
executeActions: static (diagReporter, isSupportedDiagnostic, executionData, args, cancellationToken) =>
{
var (@this, startActions, executableCodeBlocks, declaredNode, getKind, ephemeralActions) = args;
if (ephemeralActions.Any())
{
var executableNodeActionsByKind = GetNodeActionsByKind(ephemeralActions);
var syntaxNodesToAnalyze = ArrayBuilder<SyntaxNode>.GetInstance();
foreach (var block in executableCodeBlocks)
{
var filter = executionData.SemanticModel.GetSyntaxNodesToAnalyzeFilter(block, executionData.DeclaredSymbol);
if (filter is not null)
{
foreach (var descendantNode in block.DescendantNodesAndSelf(descendIntoChildren: filter))
{
if (filter(descendantNode))
syntaxNodesToAnalyze.Add(descendantNode);
}
}
else
{
syntaxNodesToAnalyze.AddRange(block.DescendantNodesAndSelf());
}
}
@this.ExecuteSyntaxNodeActions(
syntaxNodesToAnalyze, executableNodeActionsByKind, executionData,
getKind, diagReporter, isSupportedDiagnostic,
hasCodeBlockStartOrSymbolStartActions: startActions.Any(),
cancellationToken);
syntaxNodesToAnalyze.Free();
}
},
executeBlockActions: static (blockActions, diagReporter, isSupportedDiagnostic, executionData, args, cancellationToken) =>
{
var (@this, startActions, executableCodeBlocks, declaredNode, getKind, ephemeralActions) = args;
var context = new CodeBlockAnalysisContext(declaredNode, executionData.DeclaredSymbol, executionData.SemanticModel,
@this.AnalyzerOptions, diagReporter.AddDiagnosticAction, isSupportedDiagnostic, executionData.FilterSpan, executionData.IsGeneratedCode, cancellationToken);
foreach (var blockAction in blockActions)
{
@this.ExecuteAndCatchIfThrows(
blockAction.Analyzer,
static data => data.blockAction.Action(data.context),
(blockAction, context),
new AnalysisContextInfo(@this.Compilation, executionData.DeclaredSymbol, declaredNode),
cancellationToken);
}
},
argument: (@this: this, startActions, executableCodeBlocks, declaredNode, getKind, ephemeralActions),
cancellationToken);
ephemeralActions.Free();
}
/// <summary>
/// Execute operation block actions for the given analyzer for the given declaration.
/// </summary>
public void ExecuteOperationBlockActions(
ImmutableArray<OperationBlockStartAnalyzerAction> startActions,
ImmutableArray<OperationBlockAnalyzerAction> actions,
ImmutableArray<OperationBlockAnalyzerAction> endActions,
DiagnosticAnalyzer analyzer,
SyntaxNode declaredNode,
ISymbol declaredSymbol,
ImmutableArray<IOperation> operationBlocks,
ImmutableArray<IOperation> operations,
SemanticModel semanticModel,
TextSpan? filterSpan,
bool isGeneratedCode,
CancellationToken cancellationToken)
{
Debug.Assert(!operationBlocks.IsEmpty);
// The actions we discover in 'addActions' and then execute in 'executeActions'.
var ephemeralActions = ArrayBuilder<OperationAnalyzerAction>.GetInstance();
ExecuteBlockActionsCore(
startActions,
actions,
endActions,
declaredNode,
new ExecutionData(analyzer, declaredSymbol, semanticModel, filterSpan, isGeneratedCode),
addActions: static (startAction, endActions, executionData, args, cancellationToken) =>
{
var (@this, startActions, declaredNode, operationBlocks, operations, ephemeralActions) = args;
var scope = new HostOperationBlockStartAnalysisScope(startAction.Analyzer);
var startContext = new AnalyzerOperationBlockStartAnalysisContext(
scope, operationBlocks, executionData.DeclaredSymbol, executionData.SemanticModel.Compilation, @this.AnalyzerOptions,
@this.GetControlFlowGraph, declaredNode.SyntaxTree, executionData.FilterSpan, executionData.IsGeneratedCode, cancellationToken);
// Catch Exception from the start action.
@this.ExecuteAndCatchIfThrows(
startAction.Analyzer,
static args => args.startAction.Action(args.startContext),
argument: (startAction, startContext),
new AnalysisContextInfo(@this.Compilation, executionData.DeclaredSymbol),
cancellationToken);
endActions.AddAll(scope.OperationBlockEndActions);
ephemeralActions.AddRange(scope.OperationActions);
},
executeActions: static (diagReporter, isSupportedDiagnostic, executionData, args, cancellationToken) =>
{
var (@this, startActions, declaredNode, operationBlocks, operations, ephemeralActions) = args;
if (ephemeralActions.Any())
{
@this.ExecuteOperationActions(
operations, GetOperationActionsByKind(ephemeralActions),
executionData, diagReporter, isSupportedDiagnostic,
hasOperationBlockStartOrSymbolStartActions: startActions.Any(),
cancellationToken);
}
},
executeBlockActions: static (blockActions, diagReporter, isSupportedDiagnostic, executionData, args, cancellationToken) =>
{
var (@this, startActions, declaredNode, operationBlocks, operations, ephemeralActions) = args;
var context = new OperationBlockAnalysisContext(operationBlocks, executionData.DeclaredSymbol, @this.Compilation,
@this.AnalyzerOptions, diagReporter.AddDiagnosticAction, isSupportedDiagnostic, @this.GetControlFlowGraph, declaredNode.SyntaxTree,
executionData.FilterSpan, executionData.IsGeneratedCode, cancellationToken);
foreach (var blockAction in blockActions)
{
@this.ExecuteAndCatchIfThrows(
blockAction.Analyzer,
static data => data.blockAction.Action(data.context),
(blockAction, context),
new AnalysisContextInfo(@this.Compilation, executionData.DeclaredSymbol),
cancellationToken);
}
},
argument: (@this: this, startActions, declaredNode, operationBlocks, operations, ephemeralActions),
cancellationToken);
ephemeralActions.Free();
}
private void ExecuteBlockActionsCore<TBlockStartAction, TBlockAction, TArgs>(
ImmutableArray<TBlockStartAction> startActions,
ImmutableArray<TBlockAction> actions,
ImmutableArray<TBlockAction> endActions,
SyntaxNode declaredNode,
ExecutionData executionData,
Action<TBlockStartAction, HashSet<TBlockAction>, ExecutionData, TArgs, CancellationToken> addActions,
Action<AnalyzerDiagnosticReporter, Func<Diagnostic, CancellationToken, bool>, ExecutionData, TArgs, CancellationToken> executeActions,
Action<HashSet<TBlockAction>, AnalyzerDiagnosticReporter, Func<Diagnostic, CancellationToken, bool>, ExecutionData, TArgs, CancellationToken> executeBlockActions,
TArgs argument,
CancellationToken cancellationToken)
where TBlockStartAction : AnalyzerAction
where TBlockAction : AnalyzerAction
where TArgs : struct
{
Debug.Assert(declaredNode != null);
Debug.Assert(executionData.DeclaredSymbol != null);
Debug.Assert(CanHaveExecutableCodeBlock(executionData.DeclaredSymbol));
Debug.Assert(startActions.Any() || endActions.Any() || actions.Any());
if (executionData.IsGeneratedCode && _shouldSkipAnalysisOnGeneratedCode(executionData.Analyzer) ||
IsAnalyzerSuppressedForTree(executionData.Analyzer, declaredNode.SyntaxTree, cancellationToken))
{
return;
}
// Compute the sets of code block end, code block, and stateful node actions.
var blockEndActions = PooledHashSet<TBlockAction>.GetInstance();
var blockActions = PooledHashSet<TBlockAction>.GetInstance();
// Include the code block actions.
blockActions.AddAll(actions);
// Include the initial code block end actions.
blockEndActions.AddAll(endActions);
var diagReporter = GetAddSemanticDiagnostic(
executionData.SemanticModel.SyntaxTree, declaredNode.FullSpan, executionData.Analyzer, cancellationToken);
// Include the stateful actions.
foreach (var startAction in startActions)
addActions(startAction, blockEndActions, executionData, argument, cancellationToken);
using var _ = PooledDelegates.GetPooledFunction(
static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.Analyzer, d, ct),
(self: this, executionData.Analyzer),
out Func<Diagnostic, CancellationToken, bool> isSupportedDiagnostic);
// Execute stateful executable node analyzers, if any.
executeActions(diagReporter, isSupportedDiagnostic, executionData, argument, cancellationToken);
executeBlockActions(blockActions, diagReporter, isSupportedDiagnostic, executionData, argument, cancellationToken);
executeBlockActions(blockEndActions, diagReporter, isSupportedDiagnostic, executionData, argument, cancellationToken);
diagReporter.Free();
blockActions.Free();
blockEndActions.Free();
}
internal static ImmutableSegmentedDictionary<TLanguageKindEnum, ImmutableArray<SyntaxNodeAnalyzerAction<TLanguageKindEnum>>> GetNodeActionsByKind<TLanguageKindEnum>(
ArrayBuilder<SyntaxNodeAnalyzerAction<TLanguageKindEnum>> nodeActions)
where TLanguageKindEnum : struct
{
if (nodeActions.IsEmpty)
return ImmutableSegmentedDictionary<TLanguageKindEnum, ImmutableArray<SyntaxNodeAnalyzerAction<TLanguageKindEnum>>>.Empty;
var nodeActionsByKind = PooledDictionary<TLanguageKindEnum, ArrayBuilder<SyntaxNodeAnalyzerAction<TLanguageKindEnum>>>.GetInstance();
foreach (var nodeAction in nodeActions)
{
foreach (var kind in nodeAction.Kinds)
{
nodeActionsByKind.AddPooled(kind, nodeAction);
}
}
return nodeActionsByKind.ToImmutableSegmentedDictionaryAndFree();
}
/// <summary>
/// Execute syntax node actions for the given analyzer for the given declaration.
/// </summary>
public void ExecuteSyntaxNodeActions<TLanguageKindEnum>(
ArrayBuilder<SyntaxNode> nodesToAnalyze,
ImmutableSegmentedDictionary<TLanguageKindEnum, ImmutableArray<SyntaxNodeAnalyzerAction<TLanguageKindEnum>>> nodeActionsByKind,
DiagnosticAnalyzer analyzer,
SemanticModel model,
Func<SyntaxNode, TLanguageKindEnum> getKind,
TextSpan spanForContainingTopmostNodeForAnalysis,
ISymbol declaredSymbol,
TextSpan? filterSpan,
bool isGeneratedCode,
bool hasCodeBlockStartOrSymbolStartActions,
CancellationToken cancellationToken)
where TLanguageKindEnum : struct
{
if (isGeneratedCode && _shouldSkipAnalysisOnGeneratedCode(analyzer) ||
IsAnalyzerSuppressedForTree(analyzer, model.SyntaxTree, cancellationToken))
{
return;
}
var diagReporter = GetAddSemanticDiagnostic(model.SyntaxTree, spanForContainingTopmostNodeForAnalysis, analyzer, cancellationToken);
using var _ = PooledDelegates.GetPooledFunction(
static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct),
(self: this, analyzer),
out Func<Diagnostic, CancellationToken, bool> isSupportedDiagnostic);
ExecuteSyntaxNodeActions(
nodesToAnalyze, nodeActionsByKind,
new ExecutionData(analyzer, declaredSymbol, model, filterSpan, isGeneratedCode),
getKind, diagReporter, isSupportedDiagnostic, hasCodeBlockStartOrSymbolStartActions, cancellationToken);
diagReporter.Free();
}
private void ExecuteSyntaxNodeActions<TLanguageKindEnum>(
ArrayBuilder<SyntaxNode> nodesToAnalyze,
ImmutableSegmentedDictionary<TLanguageKindEnum, ImmutableArray<SyntaxNodeAnalyzerAction<TLanguageKindEnum>>> nodeActionsByKind,
ExecutionData executionData,
Func<SyntaxNode, TLanguageKindEnum> getKind,
AnalyzerDiagnosticReporter diagReporter,
Func<Diagnostic, CancellationToken, bool> isSupportedDiagnostic,
bool hasCodeBlockStartOrSymbolStartActions,
CancellationToken cancellationToken)
where TLanguageKindEnum : struct
{
Debug.Assert(nodeActionsByKind.Any());
Debug.Assert(!executionData.IsGeneratedCode || !_shouldSkipAnalysisOnGeneratedCode(executionData.Analyzer));
Debug.Assert(!IsAnalyzerSuppressedForTree(executionData.Analyzer, executionData.SemanticModel.SyntaxTree, cancellationToken));
foreach (var node in nodesToAnalyze)
{
// Most nodes have no registered actions. Check for actions before checking if the analyzer should be
// executed on the node since the generated code check in ShouldExecuteNode can be expensive in
// aggregate.
if (nodeActionsByKind.TryGetValue(getKind(node), out var actionsForKind))
{
RoslynDebug.Assert(!actionsForKind.IsEmpty, $"Unexpected empty action collection in {nameof(nodeActionsByKind)}");
if (ShouldExecuteNode(node, executionData.Analyzer, cancellationToken))
{
// If analyzer hasn't registered any CodeBlockStart or SymbolStart actions, then update the filter span
// for local diagnostics to be the callback node's full span.
// For this case, any diagnostic reported in node's callback outside it's full span will be considered
// a non-local diagnostic.
if (!hasCodeBlockStartOrSymbolStartActions)
diagReporter.FilterSpanForLocalDiagnostics = node.FullSpan;
foreach (var action in actionsForKind)
{
ExecuteSyntaxNodeAction(action, node, executionData, diagReporter.AddDiagnosticAction, isSupportedDiagnostic, cancellationToken);
}
}
}
}
}
internal static ImmutableSegmentedDictionary<OperationKind, ImmutableArray<OperationAnalyzerAction>> GetOperationActionsByKind(
ArrayBuilder<OperationAnalyzerAction> operationActions)
{
if (operationActions.IsEmpty)
return ImmutableSegmentedDictionary<OperationKind, ImmutableArray<OperationAnalyzerAction>>.Empty;
var operationActionsByKind = PooledDictionary<OperationKind, ArrayBuilder<OperationAnalyzerAction>>.GetInstance();
foreach (var operationAction in operationActions)
{
foreach (var kind in operationAction.Kinds)
{
operationActionsByKind.AddPooled(kind, operationAction);
}
}
return operationActionsByKind.ToImmutableSegmentedDictionaryAndFree();
}
/// <summary>
/// Execute operation actions for the given analyzer for the given declaration.
/// </summary>
/// <returns>
/// True, if successfully executed the actions for the given analysis scope OR all the actions have already been executed for the given analysis scope.
/// False, if there are some pending actions that are currently being executed on another thread.
/// </returns>
public void ExecuteOperationActions(
ImmutableArray<IOperation> operationsToAnalyze,
ImmutableSegmentedDictionary<OperationKind, ImmutableArray<OperationAnalyzerAction>> operationActionsByKind,
DiagnosticAnalyzer analyzer,
SemanticModel model,
TextSpan spanForContainingOperationBlock,
ISymbol declaredSymbol,
TextSpan? filterSpan,
bool isGeneratedCode,
bool hasOperationBlockStartOrSymbolStartActions,
CancellationToken cancellationToken)
{
if (isGeneratedCode && _shouldSkipAnalysisOnGeneratedCode(analyzer) ||
IsAnalyzerSuppressedForTree(analyzer, model.SyntaxTree, cancellationToken))
{
return;
}
var diagReporter = GetAddSemanticDiagnostic(model.SyntaxTree, spanForContainingOperationBlock, analyzer, cancellationToken);
using var _ = PooledDelegates.GetPooledFunction(
static (d, ct, arg) => arg.self.IsSupportedDiagnostic(arg.analyzer, d, ct),
(self: this, analyzer),
out Func<Diagnostic, CancellationToken, bool> isSupportedDiagnostic);
ExecuteOperationActions(
operationsToAnalyze, operationActionsByKind,
new ExecutionData(analyzer, declaredSymbol, model, filterSpan, isGeneratedCode),
diagReporter, isSupportedDiagnostic, hasOperationBlockStartOrSymbolStartActions, cancellationToken);
diagReporter.Free();
}
private void ExecuteOperationActions(
ImmutableArray<IOperation> operationsToAnalyze,
ImmutableSegmentedDictionary<OperationKind, ImmutableArray<OperationAnalyzerAction>> operationActionsByKind,
ExecutionData executionData,
AnalyzerDiagnosticReporter diagReporter,
Func<Diagnostic, CancellationToken, bool> isSupportedDiagnostic,
bool hasOperationBlockStartOrSymbolStartActions,
CancellationToken cancellationToken)
{
Debug.Assert(operationActionsByKind != null);
Debug.Assert(operationActionsByKind.Any());
Debug.Assert(!executionData.IsGeneratedCode || !_shouldSkipAnalysisOnGeneratedCode(executionData.Analyzer));
Debug.Assert(!IsAnalyzerSuppressedForTree(executionData.Analyzer, executionData.SemanticModel.SyntaxTree, cancellationToken));
foreach (var operation in operationsToAnalyze)
{
// Most operations have no registered actions. Check for actions before checking if the analyzer should
// be executed on the operation since the generated code check in ShouldExecuteOperation can be
// expensive in aggregate.
if (operationActionsByKind.TryGetValue(operation.Kind, out var actionsForKind))
{
RoslynDebug.Assert(!actionsForKind.IsEmpty, $"Unexpected empty action collection in {nameof(operationActionsByKind)}");
if (ShouldExecuteOperation(operation, executionData.Analyzer, cancellationToken))
{
// If analyzer hasn't registered any OperationBlockStart or SymbolStart actions, then update
// the filter span for local diagnostics to be the callback operation's full span.
// For this case, any diagnostic reported in operation's callback outside it's full span
// will be considered a non-local diagnostic.
if (!hasOperationBlockStartOrSymbolStartActions)
diagReporter.FilterSpanForLocalDiagnostics = operation.Syntax.FullSpan;
foreach (var action in actionsForKind)
{
ExecuteOperationAction(action, operation, executionData, diagReporter.AddDiagnosticAction, isSupportedDiagnostic, cancellationToken);
}
}
}
}
}
internal static bool CanHaveExecutableCodeBlock(ISymbol symbol)
{
switch (symbol.Kind)
{
case SymbolKind.Method:
case SymbolKind.Event:
case SymbolKind.Property:
case SymbolKind.NamedType:
case SymbolKind.Namespace: // We are exposing assembly/module attributes on global namespace symbol.
return true;
case SymbolKind.Field:
Debug.Assert(((IFieldSymbol)symbol).AssociatedSymbol == null);
return true;
default:
return false;
}
}
internal void ExecuteAndCatchIfThrows<TArg>(DiagnosticAnalyzer analyzer, Action<TArg> analyze, TArg argument, AnalysisContextInfo? contextInfo, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
SharedStopwatch timer = default;
if (_analyzerExecutionTimeMap != null)
{
timer = SharedStopwatch.StartNew();
}
var gate = _getAnalyzerGate(analyzer);
if (gate != null)
{
lock (gate)
{
ExecuteAndCatchIfThrows_NoLock(analyzer, analyze, argument, contextInfo, cancellationToken);
}
}
else
{
ExecuteAndCatchIfThrows_NoLock(analyzer, analyze, argument, contextInfo, cancellationToken);
}
if (_analyzerExecutionTimeMap != null)
{
var elapsed = timer.Elapsed.Ticks;
StrongBox<long> totalTicks = _analyzerExecutionTimeMap.GetOrAdd(analyzer, _ => new StrongBox<long>(0));
Interlocked.Add(ref totalTicks.Value, elapsed);
}
}
[PerformanceSensitive(
"https://github.com/dotnet/roslyn/issues/23582",
AllowCaptures = false)]
private void ExecuteAndCatchIfThrows_NoLock<TArg>(DiagnosticAnalyzer analyzer, Action<TArg> analyze, TArg argument, AnalysisContextInfo? info, CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
analyze(argument);
}
catch (Exception ex) when (HandleAnalyzerException(analyzer, ex, info) &&
HandleAnalyzerException(ex, analyzer, info, OnAnalyzerException, _analyzerExceptionFilter, cancellationToken))
{
}
}
private bool HandleAnalyzerException(DiagnosticAnalyzer analyzer, Exception ex, in AnalysisContextInfo? info)
{
if (!this.Compilation.CatchAnalyzerExceptions)
{
Debug.Assert(false);
Environment.FailFast(CreateAnalyzerExceptionDiagnostic(analyzer, ex, info).ToString());
return false;
}
return true;
}
internal static bool HandleAnalyzerException(
Exception exception,
DiagnosticAnalyzer analyzer,
AnalysisContextInfo? info,
Action<Exception, DiagnosticAnalyzer, Diagnostic, CancellationToken> onAnalyzerException,
Func<Exception, bool>? analyzerExceptionFilter,
CancellationToken cancellationToken)
{
if (!exceptionFilter(exception, analyzerExceptionFilter, cancellationToken))
{
return false;
}
// Diagnostic for analyzer exception.
var diagnostic = CreateAnalyzerExceptionDiagnostic(analyzer, exception, info);
try
{
onAnalyzerException(exception, analyzer, diagnostic, cancellationToken);
}
catch (Exception)
{
// Ignore exceptions from exception handlers.
}
return true;
static bool exceptionFilter(Exception ex, Func<Exception, bool>? analyzerExceptionFilter, CancellationToken cancellationToken)
{
if ((ex as OperationCanceledException)?.CancellationToken == cancellationToken)
{
return false;
}
if (analyzerExceptionFilter != null)
{
return analyzerExceptionFilter(ex);
}
return true;
}
}
internal static Diagnostic CreateAnalyzerExceptionDiagnostic(DiagnosticAnalyzer analyzer, Exception e, AnalysisContextInfo? info = null)
{
var analyzerName = analyzer.ToString();
var title = CodeAnalysisResources.CompilerAnalyzerFailure;
var messageFormat = CodeAnalysisResources.CompilerAnalyzerThrows;
var contextInformation = string.Join(Environment.NewLine, CreateDiagnosticDescription(info, e), CreateDisablingMessage(analyzer, analyzerName)).Trim();
var messageArguments = new[] { analyzerName, e.GetType().ToString(), e.Message, contextInformation };
var descriptor = GetAnalyzerExceptionDiagnosticDescriptor(AnalyzerExceptionDiagnosticId, title, messageFormat);
return Diagnostic.Create(descriptor, Location.None, messageArguments);
}
private static string CreateDiagnosticDescription(AnalysisContextInfo? info, Exception e)
{
if (info == null)
{
return e.CreateDiagnosticDescription();
}
return string.Join(Environment.NewLine,
string.Format(CodeAnalysisResources.ExceptionContext, info?.GetContext()), e.CreateDiagnosticDescription());
}
private static string CreateDisablingMessage(DiagnosticAnalyzer analyzer, string analyzerName)
{
var diagnosticIds = ImmutableSortedSet<string>.Empty.WithComparer(StringComparer.OrdinalIgnoreCase);
try
{
foreach (var diagnostic in analyzer.SupportedDiagnostics)
{
// If a null diagnostic is returned, we would have already reported that to the user earlier; we can just skip this.
if (diagnostic != null)
{
diagnosticIds = diagnosticIds.Add(diagnostic.Id);
}
}
}
catch (Exception ex)
{
return string.Format(CodeAnalysisResources.CompilerAnalyzerThrows, analyzerName, ex.GetType().ToString(), ex.Message, ex.CreateDiagnosticDescription());
}
if (diagnosticIds.IsEmpty)
{
return "";
}
return string.Format(CodeAnalysisResources.DisableAnalyzerDiagnosticsMessage, string.Join(", ", diagnosticIds));
}
internal static Diagnostic CreateDriverExceptionDiagnostic(Exception e)
{
var title = CodeAnalysisResources.AnalyzerDriverFailure;
var messageFormat = CodeAnalysisResources.AnalyzerDriverThrows;
var messageArguments = new[] { e.GetType().ToString(), e.Message, e.CreateDiagnosticDescription() };
var descriptor = GetAnalyzerExceptionDiagnosticDescriptor(AnalyzerDriverExceptionDiagnosticId, title, messageFormat);
return Diagnostic.Create(descriptor, Location.None, messageArguments);
}
internal static DiagnosticDescriptor GetAnalyzerExceptionDiagnosticDescriptor(string? id = null, string? title = null, string? messageFormat = null)
{
// TODO: It is not ideal to create a new descriptor per analyzer exception diagnostic instance.
// However, until we add a LongMessage field to the Diagnostic, we are forced to park the instance specific description onto the Descriptor's Description field.
// This requires us to create a new DiagnosticDescriptor instance per diagnostic instance.
id ??= AnalyzerExceptionDiagnosticId;
title ??= CodeAnalysisResources.CompilerAnalyzerFailure;
messageFormat ??= CodeAnalysisResources.CompilerAnalyzerThrows;
return new DiagnosticDescriptor(
id,
title,
messageFormat,
category: DiagnosticCategory,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
customTags: WellKnownDiagnosticTags.AnalyzerException);
}
internal static bool IsAnalyzerExceptionDiagnostic(Diagnostic diagnostic)
{
if (diagnostic.Id == AnalyzerExceptionDiagnosticId || diagnostic.Id == AnalyzerDriverExceptionDiagnosticId)
{
foreach (var tag in diagnostic.Descriptor.ImmutableCustomTags)
{
if (tag == WellKnownDiagnosticTags.AnalyzerException)
{
return true;
}
}
}
return false;
}
internal static bool AreEquivalentAnalyzerExceptionDiagnostics(Diagnostic exceptionDiagnostic, Diagnostic other)
{
// We need to have custom de-duplication logic for diagnostics generated for analyzer exceptions.
// We create a new descriptor instance per each analyzer exception diagnostic instance (see comments in method "GetAnalyzerExceptionDiagnostic" above).
// This is primarily to allow us to embed exception stack trace in the diagnostic description.
// However, this might mean that two exception diagnostics which are equivalent in terms of ID and Message, might not have equal description strings.
// We want to classify such diagnostics as equal for de-duplication purpose to reduce the noise in output.
Debug.Assert(IsAnalyzerExceptionDiagnostic(exceptionDiagnostic));
if (!IsAnalyzerExceptionDiagnostic(other))
{
return false;
}
return exceptionDiagnostic.Id == other.Id &&
exceptionDiagnostic.Severity == other.Severity &&
exceptionDiagnostic.GetMessage() == other.GetMessage();
}
private bool IsSupportedDiagnostic(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, CancellationToken cancellationToken)
{
if (diagnostic is DiagnosticWithInfo)
{
// Compiler diagnostic
return true;
}
return _analyzerManager.IsSupportedDiagnostic(analyzer, diagnostic, _isCompilerAnalyzer, this, cancellationToken);
}
private Action<Diagnostic> GetAddDiagnostic(ISymbol contextSymbol, ImmutableArray<SyntaxReference> cachedDeclaringReferences, DiagnosticAnalyzer analyzer, Func<ISymbol, SyntaxReference, Compilation, CancellationToken, SyntaxNode> getTopMostNodeForAnalysis, CancellationToken cancellationToken)
{
return GetAddDiagnostic(contextSymbol, cachedDeclaringReferences, Compilation, analyzer, _addNonCategorizedDiagnostic,
_addCategorizedLocalDiagnostic, _addCategorizedNonLocalDiagnostic, getTopMostNodeForAnalysis, _shouldSuppressGeneratedCodeDiagnostic, cancellationToken);
}
private static Action<Diagnostic> GetAddDiagnostic(
ISymbol contextSymbol,
ImmutableArray<SyntaxReference> cachedDeclaringReferences,
Compilation compilation,
DiagnosticAnalyzer analyzer,
Action<Diagnostic, CancellationToken>? addNonCategorizedDiagnostic,
Action<Diagnostic, DiagnosticAnalyzer, bool, CancellationToken>? addCategorizedLocalDiagnostic,
Action<Diagnostic, DiagnosticAnalyzer, CancellationToken>? addCategorizedNonLocalDiagnostic,
Func<ISymbol, SyntaxReference, Compilation, CancellationToken, SyntaxNode> getTopMostNodeForAnalysis,
Func<Diagnostic, DiagnosticAnalyzer, Compilation, CancellationToken, bool> shouldSuppressGeneratedCodeDiagnostic,
CancellationToken cancellationToken)
{
return diagnostic =>
{
if (shouldSuppressGeneratedCodeDiagnostic(diagnostic, analyzer, compilation, cancellationToken))
{
return;
}
if (addCategorizedLocalDiagnostic == null)
{
Debug.Assert(addNonCategorizedDiagnostic != null);
addNonCategorizedDiagnostic(diagnostic, cancellationToken);
return;
}
Debug.Assert(addNonCategorizedDiagnostic == null);
Debug.Assert(addCategorizedNonLocalDiagnostic != null);
if (diagnostic.Location.IsInSource)
{
foreach (var syntaxRef in cachedDeclaringReferences)
{
if (syntaxRef.SyntaxTree == diagnostic.Location.SourceTree)
{
var syntax = getTopMostNodeForAnalysis(contextSymbol, syntaxRef, compilation, cancellationToken);
if (diagnostic.Location.SourceSpan.IntersectsWith(syntax.FullSpan))
{
addCategorizedLocalDiagnostic(diagnostic, analyzer, false, cancellationToken);
return;
}
}
}
}
addCategorizedNonLocalDiagnostic(diagnostic, analyzer, cancellationToken);
};
}
private Action<Diagnostic> GetAddCompilationDiagnostic(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken)
{
return diagnostic =>
{
if (_shouldSuppressGeneratedCodeDiagnostic(diagnostic, analyzer, Compilation, cancellationToken))
{
return;
}
if (_addCategorizedNonLocalDiagnostic == null)
{
Debug.Assert(_addNonCategorizedDiagnostic != null);
_addNonCategorizedDiagnostic(diagnostic, cancellationToken);
return;
}
_addCategorizedNonLocalDiagnostic(diagnostic, analyzer, cancellationToken);
};
}
private AnalyzerDiagnosticReporter GetAddSemanticDiagnostic(SyntaxTree tree, DiagnosticAnalyzer analyzer, CancellationToken cancellationToken)
{
return AnalyzerDiagnosticReporter.GetInstance(new SourceOrAdditionalFile(tree), span: null, Compilation, analyzer, isSyntaxDiagnostic: false,
_addNonCategorizedDiagnostic, _addCategorizedLocalDiagnostic, _addCategorizedNonLocalDiagnostic,
_shouldSuppressGeneratedCodeDiagnostic, cancellationToken);
}
private AnalyzerDiagnosticReporter GetAddSemanticDiagnostic(SyntaxTree tree, TextSpan? span, DiagnosticAnalyzer analyzer, CancellationToken cancellationToken)
{
return AnalyzerDiagnosticReporter.GetInstance(new SourceOrAdditionalFile(tree), span, Compilation, analyzer, isSyntaxDiagnostic: false,
_addNonCategorizedDiagnostic, _addCategorizedLocalDiagnostic, _addCategorizedNonLocalDiagnostic,
_shouldSuppressGeneratedCodeDiagnostic, cancellationToken);
}
private AnalyzerDiagnosticReporter GetAddSyntaxDiagnostic(SourceOrAdditionalFile file, DiagnosticAnalyzer analyzer, CancellationToken cancellationToken)
{
return AnalyzerDiagnosticReporter.GetInstance(file, span: null, Compilation, analyzer, isSyntaxDiagnostic: true,
_addNonCategorizedDiagnostic, _addCategorizedLocalDiagnostic, _addCategorizedNonLocalDiagnostic,
_shouldSuppressGeneratedCodeDiagnostic, cancellationToken);
}
private bool ShouldExecuteNode(SyntaxNode node, DiagnosticAnalyzer analyzer, CancellationToken cancellationToken)
{
// Check if the node is generated code that must be skipped.
if (_shouldSkipAnalysisOnGeneratedCode(analyzer) &&
_isGeneratedCodeLocation(node.SyntaxTree, node.Span, cancellationToken))
{
return false;
}
return true;
}
private bool ShouldExecuteOperation(IOperation operation, DiagnosticAnalyzer analyzer, CancellationToken cancellationToken)
{
// Check if the operation syntax is generated code that must be skipped.
if (operation.Syntax != null && _shouldSkipAnalysisOnGeneratedCode(analyzer) &&
_isGeneratedCodeLocation(operation.Syntax.SyntaxTree, operation.Syntax.Span, cancellationToken))
{
return false;
}
return true;
}
internal TimeSpan ResetAnalyzerExecutionTime(DiagnosticAnalyzer analyzer)
{
Debug.Assert(_analyzerExecutionTimeMap != null);
if (!_analyzerExecutionTimeMap.TryRemove(analyzer, out var executionTime))
{
return TimeSpan.Zero;
}
return TimeSpan.FromTicks(executionTime.Value);
}
private ControlFlowGraph GetControlFlowGraphImpl(IOperation operation)
{
Debug.Assert(operation.Parent == null);
if (_lazyControlFlowGraphMap == null)
{
Interlocked.CompareExchange(ref _lazyControlFlowGraphMap, new ConcurrentDictionary<IOperation, ControlFlowGraph>(), null);
}
return _lazyControlFlowGraphMap.GetOrAdd(operation, op => ControlFlowGraphBuilder.Create(op));
}
private bool IsAnalyzerSuppressedForSymbol(DiagnosticAnalyzer analyzer, ISymbol symbol, CancellationToken cancellationToken)
{
foreach (var location in symbol.Locations)
{
if (location.SourceTree != null &&
!IsAnalyzerSuppressedForTree(analyzer, location.SourceTree, cancellationToken))
{
return false;
}
}
return true;
}
public void OnOperationBlockActionsExecuted(ImmutableArray<IOperation> operationBlocks)
{
// Clear _lazyControlFlowGraphMap entries for each operation block after we have executed
// all analysis callbacks for the given operation blocks. This avoids holding onto them
// for the entire compilation lifetime.
// These control flow graphs are created on demand and shared between flow analysis based analyzers.
if (_lazyControlFlowGraphMap?.Count > 0)
{
foreach (var operationBlock in operationBlocks)
{
var root = operationBlock.GetRootOperation();
_lazyControlFlowGraphMap.TryRemove(root, out _);
}
}
}
}
}
|