|
// 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.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Diagnostics
{
internal partial class AnalyzerManager
{
private sealed class AnalyzerExecutionContext
{
/// <summary>
/// Cached mapping of localizable strings in this descriptor to any exceptions thrown while obtaining them.
/// </summary>
private static ImmutableDictionary<LocalizableString, Exception?> s_localizableStringToException = ImmutableDictionary<LocalizableString, Exception?>.Empty.WithComparers(Roslyn.Utilities.ReferenceEqualityComparer.Instance);
private readonly DiagnosticAnalyzer _analyzer;
private readonly object _gate;
/// <summary>
/// Map from (symbol, analyzer) to count of its member symbols whose symbol declared events are not yet processed.
/// </summary>
private Dictionary<ISymbol, HashSet<ISymbol>?>? _lazyPendingMemberSymbolsMap;
/// <summary>
/// Symbol declared events for symbols with pending symbol end analysis for given analyzer.
/// </summary>
private Dictionary<ISymbol, (ImmutableArray<SymbolEndAnalyzerAction>, SymbolDeclaredCompilationEvent)>? _lazyPendingSymbolEndActionsMap;
/// <summary>
/// Task to compute HostSessionStartAnalysisScope for session wide analyzer actions, i.e. AnalyzerActions registered by analyzer's Initialize method.
/// These are run only once per every analyzer.
/// </summary>
private Task<HostSessionStartAnalysisScope>? _lazySessionScopeTask;
/// <summary>
/// Task to compute HostCompilationStartAnalysisScope for per-compilation analyzer actions, i.e. AnalyzerActions registered by analyzer's CompilationStartActions.
/// </summary>
private Task<HostCompilationStartAnalysisScope>? _lazyCompilationScopeTask;
/// <summary>
/// Task to compute HostSymbolStartAnalysisScope for per-symbol analyzer actions, i.e. AnalyzerActions registered by analyzer's SymbolStartActions.
/// </summary>
private Dictionary<ISymbol, Task<HostSymbolStartAnalysisScope>>? _lazySymbolScopeTasks;
/// <summary>
/// Supported diagnostic descriptors for diagnostic analyzer, if any.
/// </summary>
private ImmutableArray<DiagnosticDescriptor> _lazyDiagnosticDescriptors;
/// <summary>
/// Supported suppression descriptors for diagnostic suppressor, if any.
/// </summary>
private ImmutableArray<SuppressionDescriptor> _lazySuppressionDescriptors;
public AnalyzerExecutionContext(DiagnosticAnalyzer analyzer)
{
_analyzer = analyzer;
_gate = new object();
}
[PerformanceSensitive(
"https://github.com/dotnet/roslyn/issues/26778",
AllowCaptures = false)]
public Task<HostSessionStartAnalysisScope> GetSessionAnalysisScopeAsync(AnalyzerExecutor analyzerExecutor, CancellationToken cancellationToken)
{
lock (_gate)
{
Task<HostSessionStartAnalysisScope> task;
if (_lazySessionScopeTask != null)
{
return _lazySessionScopeTask;
}
task = getSessionAnalysisScopeTaskSlowAsync(this, analyzerExecutor, cancellationToken);
_lazySessionScopeTask = task;
return task;
static Task<HostSessionStartAnalysisScope> getSessionAnalysisScopeTaskSlowAsync(AnalyzerExecutionContext context, AnalyzerExecutor executor, CancellationToken cancellationToken)
{
return Task.Run(() =>
{
var sessionScope = new HostSessionStartAnalysisScope(context._analyzer);
executor.ExecuteInitializeMethod(sessionScope, executor.SeverityFilter, cancellationToken);
return sessionScope;
}, cancellationToken);
}
}
}
public void ClearSessionScopeTask()
{
lock (_gate)
{
_lazySessionScopeTask = null;
}
}
public Task<HostCompilationStartAnalysisScope> GetCompilationAnalysisScopeAsync(
HostSessionStartAnalysisScope sessionScope,
AnalyzerExecutor analyzerExecutor,
CancellationToken cancellationToken)
{
lock (_gate)
{
if (_lazyCompilationScopeTask == null)
{
_lazyCompilationScopeTask = Task.Run(() =>
{
Debug.Assert(sessionScope.Analyzer == _analyzer);
var compilationAnalysisScope = new HostCompilationStartAnalysisScope(sessionScope);
analyzerExecutor.ExecuteCompilationStartActions(sessionScope.GetAnalyzerActions().CompilationStartActions, compilationAnalysisScope, cancellationToken);
return compilationAnalysisScope;
}, cancellationToken);
}
return _lazyCompilationScopeTask;
}
}
public void ClearCompilationScopeTask()
{
lock (_gate)
{
_lazyCompilationScopeTask = null;
}
}
public Task<HostSymbolStartAnalysisScope> GetSymbolAnalysisScopeAsync(
ISymbol symbol,
bool isGeneratedCodeSymbol,
SyntaxTree? filterTree,
TextSpan? filterSpan,
ImmutableArray<SymbolStartAnalyzerAction> symbolStartActions,
AnalyzerExecutor analyzerExecutor,
CancellationToken cancellationToken)
{
lock (_gate)
{
_lazySymbolScopeTasks ??= new Dictionary<ISymbol, Task<HostSymbolStartAnalysisScope>>();
if (!_lazySymbolScopeTasks.TryGetValue(symbol, out var symbolScopeTask))
{
symbolScopeTask = Task.Run(() => getSymbolAnalysisScopeCore(), cancellationToken);
_lazySymbolScopeTasks.Add(symbol, symbolScopeTask);
}
return symbolScopeTask;
HostSymbolStartAnalysisScope getSymbolAnalysisScopeCore()
{
var symbolAnalysisScope = new HostSymbolStartAnalysisScope(_analyzer);
analyzerExecutor.ExecuteSymbolStartActions(symbol, symbolStartActions, symbolAnalysisScope, isGeneratedCodeSymbol, filterTree, filterSpan, cancellationToken);
var symbolEndActions = symbolAnalysisScope.GetAnalyzerActions();
if (symbolEndActions.SymbolEndActionsCount > 0)
{
var dependentSymbols = getDependentSymbols();
lock (_gate)
{
_lazyPendingMemberSymbolsMap ??= new Dictionary<ISymbol, HashSet<ISymbol>?>();
// Guard against entry added from another thread.
VerifyNewEntryForPendingMemberSymbolsMap(symbol, dependentSymbols);
_lazyPendingMemberSymbolsMap[symbol] = dependentSymbols;
}
}
return symbolAnalysisScope;
}
}
HashSet<ISymbol>? getDependentSymbols()
{
HashSet<ISymbol>? memberSet = null;
switch (symbol.Kind)
{
case SymbolKind.NamedType:
processMembers(((INamedTypeSymbol)symbol).GetMembers());
break;
case SymbolKind.Namespace:
processMembers(((INamespaceSymbol)symbol).GetMembers());
break;
}
return memberSet;
void processMembers(IEnumerable<ISymbol> members)
{
foreach (var member in members)
{
if (!member.IsImplicitlyDeclared && member.IsInSource())
{
memberSet ??= new HashSet<ISymbol>();
memberSet.Add(member);
if (member is IMethodSymbol { PartialImplementationPart: { } methodImplementation })
memberSet.Add(methodImplementation);
else if (member is IPropertySymbol { PartialImplementationPart: { } propertyImplementation })
memberSet.Add(propertyImplementation);
}
if (member is INamedTypeSymbol typeMember)
{
processMembers(typeMember.GetMembers());
}
}
}
}
}
[Conditional("DEBUG")]
private void VerifyNewEntryForPendingMemberSymbolsMap(ISymbol symbol, HashSet<ISymbol>? dependentSymbols)
{
RoslynDebug.Assert(_lazyPendingMemberSymbolsMap != null, $"{nameof(_lazyPendingMemberSymbolsMap)} was expected to be a non-null value.");
if (_lazyPendingMemberSymbolsMap.TryGetValue(symbol, out var existingDependentSymbols))
{
if (existingDependentSymbols == null)
{
RoslynDebug.Assert(dependentSymbols == null, $"{nameof(dependentSymbols)} was expected to be null.");
}
else
{
RoslynDebug.Assert(dependentSymbols != null, $"{nameof(dependentSymbols)} was expected to be a non-null value.");
RoslynDebug.Assert(existingDependentSymbols.IsSubsetOf(dependentSymbols), $"{nameof(existingDependentSymbols)} was expected to be a subset of {nameof(dependentSymbols)}");
}
}
}
public void ClearSymbolScopeTask(ISymbol symbol)
{
lock (_gate)
{
_lazySymbolScopeTasks?.Remove(symbol);
}
}
public ImmutableArray<DiagnosticDescriptor> GetOrComputeDiagnosticDescriptors(DiagnosticAnalyzer analyzer, AnalyzerExecutor analyzerExecutor, CancellationToken cancellationToken)
=> GetOrComputeDescriptors(ref _lazyDiagnosticDescriptors, ComputeDiagnosticDescriptors_NoLock, analyzer, analyzerExecutor, _gate, cancellationToken);
public ImmutableArray<SuppressionDescriptor> GetOrComputeSuppressionDescriptors(DiagnosticSuppressor suppressor, AnalyzerExecutor analyzerExecutor, CancellationToken cancellationToken)
=> GetOrComputeDescriptors(ref _lazySuppressionDescriptors, ComputeSuppressionDescriptors_NoLock, suppressor, analyzerExecutor, _gate, cancellationToken);
private static ImmutableArray<TDescriptor> GetOrComputeDescriptors<TDescriptor>(
ref ImmutableArray<TDescriptor> lazyDescriptors,
Func<DiagnosticAnalyzer, AnalyzerExecutor, CancellationToken, ImmutableArray<TDescriptor>> computeDescriptorsNoLock,
DiagnosticAnalyzer analyzer,
AnalyzerExecutor analyzerExecutor,
object gate,
CancellationToken cancellationToken)
{
if (!lazyDescriptors.IsDefault)
{
return lazyDescriptors;
}
lock (gate)
{
// We re-check if lazyDescriptors is default inside the lock statement
// to ensure that we don't invoke 'computeDescriptorsNoLock' multiple times.
// 'computeDescriptorsNoLock' makes analyzer callbacks and these can throw
// exceptions, leading to AD0001 diagnostics and duplicate callbacks can
// lead to duplicate AD0001 diagnostics.
if (lazyDescriptors.IsDefault)
{
lazyDescriptors = computeDescriptorsNoLock(analyzer, analyzerExecutor, cancellationToken);
}
return lazyDescriptors;
}
}
/// <summary>
/// Compute <see cref="DiagnosticAnalyzer.SupportedDiagnostics"/> and exception handler for the given <paramref name="analyzer"/>.
/// </summary>
private static ImmutableArray<DiagnosticDescriptor> ComputeDiagnosticDescriptors_NoLock(
DiagnosticAnalyzer analyzer,
AnalyzerExecutor analyzerExecutor,
CancellationToken cancellationToken)
{
var supportedDiagnostics = ImmutableArray<DiagnosticDescriptor>.Empty;
// Catch Exception from analyzer.SupportedDiagnostics
analyzerExecutor.ExecuteAndCatchIfThrows(
analyzer,
_ =>
{
var supportedDiagnosticsLocal = analyzer.SupportedDiagnostics;
if (!supportedDiagnosticsLocal.IsDefaultOrEmpty)
{
foreach (var descriptor in supportedDiagnosticsLocal)
{
if (descriptor == null)
{
// Disallow null descriptors.
throw new ArgumentException(string.Format(CodeAnalysisResources.SupportedDiagnosticsHasNullDescriptor, analyzer.ToString()), nameof(DiagnosticAnalyzer.SupportedDiagnostics));
}
}
supportedDiagnostics = supportedDiagnosticsLocal;
}
},
argument: (object?)null,
contextInfo: null,
cancellationToken);
// Force evaluate and report exception diagnostics from LocalizableString.ToString().
var onAnalyzerException = analyzerExecutor.OnAnalyzerException;
if (onAnalyzerException != null)
{
foreach (var descriptor in supportedDiagnostics)
{
// Compute the localizable strings once, caching any exceptions produced doing that. This helps
// avoid an excessive amount of string allocations loading resources.
forceLocalizableStringExceptions(descriptor.Title);
forceLocalizableStringExceptions(descriptor.MessageFormat);
forceLocalizableStringExceptions(descriptor.Description);
}
}
return supportedDiagnostics;
void forceLocalizableStringExceptions(LocalizableString localizableString)
{
var exception = getAndCacheToStringException(localizableString);
if (exception != null)
{
var diagnostic = AnalyzerExecutor.CreateAnalyzerExceptionDiagnostic(analyzer, exception);
onAnalyzerException(exception, analyzer, diagnostic, cancellationToken);
}
}
static Exception? getAndCacheToStringException(LocalizableString localizableString)
{
if (!localizableString.CanThrowExceptions)
return null;
return ImmutableInterlocked.GetOrAdd(ref s_localizableStringToException, localizableString, computeException);
static Exception? computeException(LocalizableString localizableString)
{
Exception? localException = null;
EventHandler<Exception> handler = (_, ex) => localException = ex;
localizableString.OnException += handler;
localizableString.ToString();
localizableString.OnException -= handler;
return localException;
}
}
}
private static ImmutableArray<SuppressionDescriptor> ComputeSuppressionDescriptors_NoLock(
DiagnosticAnalyzer analyzer,
AnalyzerExecutor analyzerExecutor,
CancellationToken cancellationToken)
{
var descriptors = ImmutableArray<SuppressionDescriptor>.Empty;
if (analyzer is DiagnosticSuppressor suppressor)
{
// Catch Exception from suppressor.SupportedSuppressions
analyzerExecutor.ExecuteAndCatchIfThrows(
analyzer,
_ =>
{
var descriptorsLocal = suppressor.SupportedSuppressions;
if (!descriptorsLocal.IsDefaultOrEmpty)
{
foreach (var descriptor in descriptorsLocal)
{
if (descriptor == null)
{
// Disallow null descriptors.
throw new ArgumentException(string.Format(CodeAnalysisResources.SupportedSuppressionsHasNullDescriptor, analyzer.ToString()), nameof(DiagnosticSuppressor.SupportedSuppressions));
}
}
descriptors = descriptorsLocal;
}
},
argument: (object?)null,
contextInfo: null,
cancellationToken);
}
return descriptors;
}
public bool TryProcessCompletedMemberAndGetPendingSymbolEndActionsForContainer(
ISymbol containingSymbol,
ISymbol processedMemberSymbol,
out (ImmutableArray<SymbolEndAnalyzerAction> symbolEndActions, SymbolDeclaredCompilationEvent symbolDeclaredEvent) containerEndActionsAndEvent)
{
containerEndActionsAndEvent = default;
lock (_gate)
{
if (_lazyPendingMemberSymbolsMap == null ||
!_lazyPendingMemberSymbolsMap.TryGetValue(containingSymbol, out var pendingMemberSymbols))
{
return false;
}
Debug.Assert(pendingMemberSymbols != null);
var removed = pendingMemberSymbols.Remove(processedMemberSymbol);
if (pendingMemberSymbols.Count > 0 ||
_lazyPendingSymbolEndActionsMap == null ||
!_lazyPendingSymbolEndActionsMap.TryGetValue(containingSymbol, out containerEndActionsAndEvent))
{
return false;
}
_lazyPendingSymbolEndActionsMap.Remove(containingSymbol);
return true;
}
}
public bool TryStartExecuteSymbolEndActions(ImmutableArray<SymbolEndAnalyzerAction> symbolEndActions, SymbolDeclaredCompilationEvent symbolDeclaredEvent)
{
Debug.Assert(!symbolEndActions.IsEmpty);
var symbol = symbolDeclaredEvent.Symbol;
lock (_gate)
{
Debug.Assert(_lazyPendingMemberSymbolsMap != null);
if (_lazyPendingMemberSymbolsMap.TryGetValue(symbol, out var pendingMemberSymbols) &&
pendingMemberSymbols?.Count > 0)
{
// At least one member is not complete, so mark the event for later processing of symbol end actions.
MarkSymbolEndAnalysisPending_NoLock(symbol, symbolEndActions, symbolDeclaredEvent);
return false;
}
// Try remove the pending event in case it was marked pending by another thread when members were not yet complete.
_lazyPendingSymbolEndActionsMap?.Remove(symbol);
return true;
}
}
public void MarkSymbolEndAnalysisComplete(ISymbol symbol)
{
lock (_gate)
{
_lazyPendingMemberSymbolsMap?.Remove(symbol);
}
}
public void MarkSymbolEndAnalysisPending(ISymbol symbol, ImmutableArray<SymbolEndAnalyzerAction> symbolEndActions, SymbolDeclaredCompilationEvent symbolDeclaredEvent)
{
lock (_gate)
{
MarkSymbolEndAnalysisPending_NoLock(symbol, symbolEndActions, symbolDeclaredEvent);
}
}
private void MarkSymbolEndAnalysisPending_NoLock(ISymbol symbol, ImmutableArray<SymbolEndAnalyzerAction> symbolEndActions, SymbolDeclaredCompilationEvent symbolDeclaredEvent)
{
_lazyPendingSymbolEndActionsMap ??= new Dictionary<ISymbol, (ImmutableArray<SymbolEndAnalyzerAction>, SymbolDeclaredCompilationEvent)>();
_lazyPendingSymbolEndActionsMap[symbol] = (symbolEndActions, symbolDeclaredEvent);
}
[Conditional("DEBUG")]
public void VerifyAllSymbolEndActionsExecuted()
{
lock (_gate)
{
Debug.Assert(_lazyPendingMemberSymbolsMap == null || _lazyPendingMemberSymbolsMap.Count == 0);
Debug.Assert(_lazyPendingSymbolEndActionsMap == null || _lazyPendingSymbolEndActionsMap.Count == 0);
}
}
}
}
}
|