|
// 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.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;
namespace Microsoft.CodeAnalysis.Diagnostics;
/// <summary>
/// Provides and caches information about diagnostic analyzers such as <see cref="AnalyzerReference"/>,
/// <see cref="DiagnosticAnalyzer"/> instance, <see cref="DiagnosticDescriptor"/>s.
/// Thread-safe.
/// </summary>
internal sealed partial class DiagnosticAnalyzerInfoCache
{
/// <summary>
/// Supported descriptors of each <see cref="DiagnosticAnalyzer"/>.
/// </summary>
/// <remarks>
/// Holds on <see cref="DiagnosticAnalyzer"/> instances weakly so that we don't keep analyzers coming from package references alive.
/// They need to be released when the project stops referencing the analyzer.
///
/// The purpose of this map is to avoid multiple calls to <see cref="DiagnosticAnalyzer.SupportedDiagnostics"/> that might return different values
/// (they should not but we need a guarantee to function correctly).
/// </remarks>
private readonly ConditionalWeakTable<DiagnosticAnalyzer, DiagnosticDescriptorsInfo> _descriptorsInfo;
/// <summary>
/// Supported suppressions of each <see cref="DiagnosticSuppressor"/>.
/// </summary>
/// <remarks>
/// Holds on <see cref="DiagnosticSuppressor"/> instances weakly so that we don't keep suppressors coming from package references alive.
/// They need to be released when the project stops referencing the suppressor.
///
/// The purpose of this map is to avoid multiple calls to <see cref="DiagnosticSuppressor.SupportedSuppressions"/> that might return different values
/// (they should not but we need a guarantee to function correctly).
/// </remarks>
private readonly ConditionalWeakTable<DiagnosticSuppressor, SuppressionDescriptorsInfo> _suppressionsInfo;
/// <summary>
/// Lazily populated map from diagnostic IDs to diagnostic descriptor.
/// If same diagnostic ID is reported by multiple descriptors, a null value is stored in the map for that ID.
/// </summary>
private readonly ConcurrentDictionary<string, DiagnosticDescriptor?> _idToDescriptorsMap;
private sealed class DiagnosticDescriptorsInfo(ImmutableArray<DiagnosticDescriptor> supportedDescriptors, bool telemetryAllowed)
{
public readonly ImmutableArray<DiagnosticDescriptor> SupportedDescriptors = supportedDescriptors;
public readonly bool TelemetryAllowed = telemetryAllowed;
public readonly bool HasCompilationEndDescriptor = supportedDescriptors.Any(DiagnosticDescriptorExtensions.IsCompilationEnd);
}
private sealed class SuppressionDescriptorsInfo(ImmutableArray<SuppressionDescriptor> supportedSuppressions)
{
public readonly ImmutableArray<SuppressionDescriptor> SupportedSuppressions = supportedSuppressions;
}
[Export, Shared]
internal sealed class SharedGlobalCache
{
public readonly DiagnosticAnalyzerInfoCache AnalyzerInfoCache = new();
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public SharedGlobalCache()
{
}
}
internal DiagnosticAnalyzerInfoCache()
{
_descriptorsInfo = new();
_suppressionsInfo = new();
_idToDescriptorsMap = [];
}
/// <summary>
/// Returns <see cref="DiagnosticAnalyzer.SupportedDiagnostics"/> of given <paramref name="analyzer"/>.
/// </summary>
public ImmutableArray<DiagnosticDescriptor> GetDiagnosticDescriptors(DiagnosticAnalyzer analyzer)
=> GetOrCreateDescriptorsInfo(analyzer).SupportedDescriptors;
/// <summary>
/// Returns <see cref="DiagnosticSuppressor.SupportedSuppressions"/> of given <paramref name="suppressor"/>.
/// </summary>
public ImmutableArray<SuppressionDescriptor> GetDiagnosticSuppressions(DiagnosticSuppressor suppressor)
=> GetOrCreateSuppressionsInfo(suppressor).SupportedSuppressions;
/// <summary>
/// Returns <see cref="DiagnosticAnalyzer.SupportedDiagnostics"/> of given <paramref name="analyzer"/>
/// that are not compilation end descriptors.
/// </summary>
public ImmutableArray<DiagnosticDescriptor> GetNonCompilationEndDiagnosticDescriptors(DiagnosticAnalyzer analyzer)
{
var descriptorInfo = GetOrCreateDescriptorsInfo(analyzer);
return !descriptorInfo.HasCompilationEndDescriptor
? descriptorInfo.SupportedDescriptors
: descriptorInfo.SupportedDescriptors.WhereAsArray(d => !d.IsCompilationEnd());
}
/// <summary>
/// Returns <see cref="DiagnosticAnalyzer.SupportedDiagnostics"/> of given <paramref name="analyzer"/>
/// that are compilation end descriptors.
/// </summary>
public ImmutableArray<DiagnosticDescriptor> GetCompilationEndDiagnosticDescriptors(DiagnosticAnalyzer analyzer)
{
var descriptorInfo = GetOrCreateDescriptorsInfo(analyzer);
return descriptorInfo.HasCompilationEndDescriptor
? descriptorInfo.SupportedDescriptors.WhereAsArray(d => d.IsCompilationEnd())
: [];
}
/// <summary>
/// Returns true if given <paramref name="analyzer"/> has a compilation end descriptor
/// that is reported in the Compilation end action.
/// </summary>
public bool IsCompilationEndAnalyzer(DiagnosticAnalyzer analyzer)
=> GetOrCreateDescriptorsInfo(analyzer).HasCompilationEndDescriptor;
/// <summary>
/// Determine whether collection of telemetry is allowed for given <paramref name="analyzer"/>.
/// </summary>
public bool IsTelemetryCollectionAllowed(DiagnosticAnalyzer analyzer)
=> GetOrCreateDescriptorsInfo(analyzer).TelemetryAllowed;
public bool TryGetDescriptorForDiagnosticId(string diagnosticId, [NotNullWhen(true)] out DiagnosticDescriptor? descriptor)
=> _idToDescriptorsMap.TryGetValue(diagnosticId, out descriptor) && descriptor != null;
private DiagnosticDescriptorsInfo GetOrCreateDescriptorsInfo(DiagnosticAnalyzer analyzer)
=> _descriptorsInfo.GetValue(analyzer, CalculateDescriptorsInfo);
private DiagnosticDescriptorsInfo CalculateDescriptorsInfo(DiagnosticAnalyzer analyzer)
{
ImmutableArray<DiagnosticDescriptor> descriptors;
try
{
// SupportedDiagnostics is user code and can throw an exception.
descriptors = analyzer.SupportedDiagnostics.NullToEmpty();
}
catch
{
// No need to report the exception to the user.
// Eventually, when the analyzer runs the compiler analyzer driver will report a diagnostic.
descriptors = [];
}
PopulateIdToDescriptorMap(descriptors);
var telemetryAllowed = IsTelemetryCollectionAllowed(analyzer, descriptors);
return new DiagnosticDescriptorsInfo(descriptors, telemetryAllowed);
}
private SuppressionDescriptorsInfo GetOrCreateSuppressionsInfo(DiagnosticSuppressor suppressor)
=> _suppressionsInfo.GetValue(suppressor, CalculateSuppressionsInfo);
private SuppressionDescriptorsInfo CalculateSuppressionsInfo(DiagnosticSuppressor suppressor)
{
ImmutableArray<SuppressionDescriptor> suppressions;
try
{
// SupportedSuppressions is user code and can throw an exception.
suppressions = suppressor.SupportedSuppressions.NullToEmpty();
}
catch
{
// No need to report the exception to the user.
// Eventually, when the suppressor runs the compiler analyzer driver will report a diagnostic.
suppressions = [];
}
return new SuppressionDescriptorsInfo(suppressions);
}
private static bool IsTelemetryCollectionAllowed(DiagnosticAnalyzer analyzer, ImmutableArray<DiagnosticDescriptor> descriptors)
=> analyzer.IsCompilerAnalyzer() ||
analyzer is IBuiltInAnalyzer ||
descriptors.Length > 0 && descriptors[0].ImmutableCustomTags().Any(static t => t == WellKnownDiagnosticTags.Telemetry);
private void PopulateIdToDescriptorMap(ImmutableArray<DiagnosticDescriptor> descriptors)
{
foreach (var descriptor in descriptors)
{
if (!_idToDescriptorsMap.TryGetValue(descriptor.Id, out var existingDescriptor))
{
_idToDescriptorsMap[descriptor.Id] = descriptor;
}
else if (existingDescriptor != null && !descriptor.Equals(existingDescriptor))
{
// Multiple descriptors with same diagnostic ID, store null in the map.
// Exception case: Many CAxxxx analyzers use multiple descriptors with same ID which differ only in MessageFormat.
// This allows analyzer to report slightly differing diagnostic messages with same ID.
// We handle this case here by allowing existing descriptor to be used.
if (descriptor.WithMessageFormat(existingDescriptor.MessageFormat).Equals(existingDescriptor))
continue;
_idToDescriptorsMap[descriptor.Id] = null;
}
}
}
}
|