|
// 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.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.PooledObjects;
#if !NETCOREAPP
using Roslyn.Utilities;
#endif
namespace Microsoft.CodeAnalysis.Diagnostics;
/// <summary>
/// Information about analyzers supplied by the host (IDE), which can be completely skipped or its diagnostics partially filtered for the corresponding project
/// as project analyzer reference (from NuGet) has equivalent analyzer(s) reporting all or subset of diagnostic IDs reported by these analyzers.
/// </summary>
internal readonly struct SkippedHostAnalyzersInfo
{
public static readonly SkippedHostAnalyzersInfo Empty = new(
[],
ImmutableDictionary<DiagnosticAnalyzer, ImmutableArray<string>>.Empty);
/// <summary>
/// Analyzers supplied by the host (IDE), which can be completely skipped for the corresponding project
/// as project analyzer reference has equivalent analyzer(s) reporting all diagnostic IDs reported by these analyzers.
/// </summary>
public ImmutableHashSet<DiagnosticAnalyzer> SkippedAnalyzers { get; }
/// <summary>
/// Analyzer to diagnostic ID map, such that the diagnostics of those IDs reported by the analyzer should be filtered
/// for a correspndiong project.
/// This includes the analyzers supplied by the host (IDE), such that project's analyzer references (from NuGet)
/// has equivalent analyzer(s) reporting subset of diagnostic IDs reported by these analyzers.
/// </summary>
public ImmutableDictionary<DiagnosticAnalyzer, ImmutableArray<string>> FilteredDiagnosticIdsForAnalyzers { get; }
private SkippedHostAnalyzersInfo(
ImmutableHashSet<DiagnosticAnalyzer> skippedHostAnalyzers,
ImmutableDictionary<DiagnosticAnalyzer, ImmutableArray<string>> filteredDiagnosticIdsForAnalyzers)
{
SkippedAnalyzers = skippedHostAnalyzers;
FilteredDiagnosticIdsForAnalyzers = filteredDiagnosticIdsForAnalyzers;
}
public static SkippedHostAnalyzersInfo Create(
HostDiagnosticAnalyzers hostAnalyzers,
IReadOnlyList<AnalyzerReference> projectAnalyzerReferences,
string language,
DiagnosticAnalyzerInfoCache analyzerInfoCache)
{
using var _1 = PooledHashSet<object>.GetInstance(out var projectAnalyzerIds);
using var _2 = PooledHashSet<string>.GetInstance(out var projectAnalyzerDiagnosticIds);
using var _3 = PooledHashSet<string>.GetInstance(out var projectSuppressedDiagnosticIds);
foreach (var (analyzerId, analyzers) in hostAnalyzers.CreateProjectDiagnosticAnalyzersPerReference(projectAnalyzerReferences, language))
{
projectAnalyzerIds.Add(analyzerId);
foreach (var analyzer in analyzers)
{
foreach (var descriptor in analyzerInfoCache.GetDiagnosticDescriptors(analyzer))
{
projectAnalyzerDiagnosticIds.Add(descriptor.Id);
}
if (analyzer is DiagnosticSuppressor suppressor)
{
foreach (var descriptor in analyzerInfoCache.GetDiagnosticSuppressions(suppressor))
{
projectSuppressedDiagnosticIds.Add(descriptor.SuppressedDiagnosticId);
}
}
}
}
if (projectAnalyzerIds.Count == 0)
{
return Empty;
}
var fullySkippedHostAnalyzersBuilder = ImmutableHashSet.CreateBuilder<DiagnosticAnalyzer>();
var partiallySkippedHostAnalyzersBuilder = ImmutableDictionary.CreateBuilder<DiagnosticAnalyzer, ImmutableArray<string>>();
foreach (var (hostAnalyzerId, analyzers) in hostAnalyzers.GetOrCreateHostDiagnosticAnalyzersPerReference(language))
{
foreach (var hostAnalyzer in analyzers)
{
if (projectAnalyzerIds.Contains(hostAnalyzerId))
{
// Duplicate project and host analyzer.
// Do not mark this as a skipped host analyzer as that will also cause the project analyzer to be skipped.
// We already perform the required host analyzer reference de-duping in the executor.
continue;
}
if (!ShouldIncludeHostAnalyzer(hostAnalyzer, projectAnalyzerDiagnosticIds, projectSuppressedDiagnosticIds, analyzerInfoCache, out var skippedIdsForAnalyzer))
{
fullySkippedHostAnalyzersBuilder.Add(hostAnalyzer);
}
else if (skippedIdsForAnalyzer.Length > 0)
{
partiallySkippedHostAnalyzersBuilder.Add(hostAnalyzer, skippedIdsForAnalyzer);
}
}
}
var fullySkippedHostAnalyzers = fullySkippedHostAnalyzersBuilder.ToImmutable();
var filteredDiagnosticIdsForAnalyzers = partiallySkippedHostAnalyzersBuilder.ToImmutable();
if (fullySkippedHostAnalyzers.IsEmpty && filteredDiagnosticIdsForAnalyzers.IsEmpty)
{
return Empty;
}
return new SkippedHostAnalyzersInfo(fullySkippedHostAnalyzers, filteredDiagnosticIdsForAnalyzers);
static bool ShouldIncludeHostAnalyzer(
DiagnosticAnalyzer hostAnalyzer,
HashSet<string> projectAnalyzerDiagnosticIds,
HashSet<string> projectSuppressedDiagnosticIds,
DiagnosticAnalyzerInfoCache analyzerInfoCache,
out ImmutableArray<string> skippedDiagnosticIdsForAnalyzer)
{
// Include only those host (VSIX) analyzers that report at least one unique diagnostic ID
// which is not reported by any project (NuGet) analyzer.
// See https://github.com/dotnet/roslyn/issues/18818.
var shouldInclude = false;
var descriptors = analyzerInfoCache.GetDiagnosticDescriptors(hostAnalyzer);
var skippedDiagnosticIdsBuilder = ArrayBuilder<string>.GetInstance();
foreach (var descriptor in descriptors)
{
if (projectAnalyzerDiagnosticIds.Contains(descriptor.Id))
{
skippedDiagnosticIdsBuilder.Add(descriptor.Id);
}
else
{
shouldInclude = true;
}
}
if (hostAnalyzer is DiagnosticSuppressor suppressor)
{
skippedDiagnosticIdsForAnalyzer = [];
// Only execute host suppressor if it does not suppress any diagnostic ID reported by project analyzer
// and does not share any suppression ID with a project suppressor.
foreach (var descriptor in analyzerInfoCache.GetDiagnosticSuppressions(suppressor))
{
if (projectAnalyzerDiagnosticIds.Contains(descriptor.SuppressedDiagnosticId) ||
projectSuppressedDiagnosticIds.Contains(descriptor.SuppressedDiagnosticId))
{
return false;
}
}
return true;
}
skippedDiagnosticIdsForAnalyzer = skippedDiagnosticIdsBuilder.ToImmutableAndFree();
return shouldInclude;
}
}
}
|