// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis.Diagnostics.Log;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Diagnostics
{
///
/// This owns every information about analyzer itself
/// such as , and .
///
/// people should use this to get s or s of a .
/// this will do appropriate de-duplication and cache for those information.
///
/// this should be always thread-safe.
///
internal sealed partial class HostAnalyzerManager
{
///
/// This contains vsix info on where comes from.
///
private readonly Lazy> _hostDiagnosticAnalyzerPackages;
///
/// Key is analyzer reference identity .
///
/// We use the key to de-duplicate analyzer references if they are referenced from multiple places.
///
private readonly Lazy> _hostAnalyzerReferencesMap;
///
/// Key is the language the supports and key for the second map is analyzer reference identity and
/// for that assembly reference.
///
/// Entry will be lazily filled in.
///
private readonly ConcurrentDictionary>> _hostDiagnosticAnalyzersPerLanguageMap;
///
/// Key is analyzer reference identity .
///
/// Value is set of that belong to the .
///
/// We populate it lazily. otherwise, we will bring in all analyzers preemptively
///
private readonly Lazy>> _lazyHostDiagnosticAnalyzersPerReferenceMap;
///
/// Host diagnostic update source for analyzer host specific diagnostics.
///
private readonly AbstractHostDiagnosticUpdateSource _hostDiagnosticUpdateSource;
///
/// map to compiler diagnostic analyzer.
///
private ImmutableDictionary _compilerDiagnosticAnalyzerMap;
///
/// map from host diagnostic analyzer to package name it came from
///
private ImmutableDictionary _hostDiagnosticAnalyzerPackageNameMap;
///
/// map to compiler diagnostic analyzer descriptor.
///
private ImmutableDictionary> _compilerDiagnosticAnalyzerDescriptorMap;
///
/// Cache from instance to its supported descriptors.
///
private readonly ConditionalWeakTable> _descriptorCache;
public HostAnalyzerManager(Lazy> hostAnalyzerPackages, IAnalyzerAssemblyLoader hostAnalyzerAssemblyLoader, AbstractHostDiagnosticUpdateSource hostDiagnosticUpdateSource, PrimaryWorkspace primaryWorkspace) :
this(new Lazy>(() => CreateAnalyzerReferencesFromPackages(hostAnalyzerPackages.Value, new HostAnalyzerReferenceDiagnosticReporter(hostDiagnosticUpdateSource, primaryWorkspace), hostAnalyzerAssemblyLoader), isThreadSafe: true),
hostAnalyzerPackages, hostDiagnosticUpdateSource)
{
}
private HostAnalyzerManager(
Lazy> hostAnalyzerReferences, Lazy> hostAnalyzerPackages, AbstractHostDiagnosticUpdateSource hostDiagnosticUpdateSource)
{
_hostDiagnosticAnalyzerPackages = hostAnalyzerPackages;
_hostDiagnosticUpdateSource = hostDiagnosticUpdateSource;
_hostAnalyzerReferencesMap = new Lazy>(() => hostAnalyzerReferences.Value.IsDefaultOrEmpty ? ImmutableDictionary.Empty : CreateAnalyzerReferencesMap(hostAnalyzerReferences.Value), isThreadSafe: true);
_hostDiagnosticAnalyzersPerLanguageMap = new ConcurrentDictionary>>(concurrencyLevel: 2, capacity: 2);
_lazyHostDiagnosticAnalyzersPerReferenceMap = new Lazy>>(() => CreateDiagnosticAnalyzersPerReferenceMap(_hostAnalyzerReferencesMap.Value), isThreadSafe: true);
_compilerDiagnosticAnalyzerMap = ImmutableDictionary.Empty;
_compilerDiagnosticAnalyzerDescriptorMap = ImmutableDictionary>.Empty;
_hostDiagnosticAnalyzerPackageNameMap = ImmutableDictionary.Empty;
_descriptorCache = new ConditionalWeakTable>();
}
// this is for testing
internal HostAnalyzerManager(ImmutableArray hostAnalyzerReferences, AbstractHostDiagnosticUpdateSource hostDiagnosticUpdateSource) :
this(new Lazy>(() => hostAnalyzerReferences), new Lazy>(() => ImmutableArray.Empty), hostDiagnosticUpdateSource)
{
}
///
/// It returns a string that can be used as a way to de-duplicate s.
///
public object GetAnalyzerReferenceIdentity(AnalyzerReference reference)
{
return reference.Id;
}
///
/// It returns a map with as key and as value
///
public ImmutableDictionary CreateAnalyzerReferencesMap(Project projectOpt = null)
{
if (projectOpt == null)
{
return _hostAnalyzerReferencesMap.Value;
}
return _hostAnalyzerReferencesMap.Value.AddRange(CreateProjectAnalyzerReferencesMap(projectOpt));
}
///
/// Return of given .
///
public ImmutableArray GetDiagnosticDescriptors(DiagnosticAnalyzer analyzer)
{
return (ImmutableArray)_descriptorCache.GetValue(analyzer, a => GetDiagnosticDescriptorsCore(a));
}
private ImmutableArray GetDiagnosticDescriptorsCore(DiagnosticAnalyzer analyzer)
{
ImmutableArray descriptors;
try
{
// SupportedDiagnostics is user code and can throw an exception.
descriptors = analyzer.SupportedDiagnostics;
if (descriptors.IsDefault)
{
descriptors = ImmutableArray.Empty;
}
}
catch (Exception ex)
{
AnalyzerHelper.OnAnalyzerExceptionForSupportedDiagnostics(analyzer, ex, _hostDiagnosticUpdateSource);
descriptors = ImmutableArray.Empty;
}
return descriptors;
}
///
/// Return true if the given is suppressed for the given project.
///
public bool IsAnalyzerSuppressed(DiagnosticAnalyzer analyzer, Project project)
{
var options = project.CompilationOptions;
if (options == null || IsCompilerDiagnosticAnalyzer(project.Language, analyzer))
{
return false;
}
// don't capture project
var projectId = project.Id;
// Skip telemetry logging for supported diagnostics, as that can cause an infinite loop.
void onAnalyzerException(Exception ex, DiagnosticAnalyzer a, Diagnostic diagnostic) =>
AnalyzerHelper.OnAnalyzerException_NoTelemetryLogging(ex, a, diagnostic, _hostDiagnosticUpdateSource, projectId);
return CompilationWithAnalyzers.IsDiagnosticAnalyzerSuppressed(analyzer, options, onAnalyzerException);
}
///
/// Get identity and s map for given
///
public ImmutableDictionary> GetHostDiagnosticAnalyzersPerReference(string language)
{
return _hostDiagnosticAnalyzersPerLanguageMap.GetOrAdd(language, CreateHostDiagnosticAnalyzersAndBuildMap);
}
///
/// Create identity and s map
///
public ImmutableDictionary> GetHostDiagnosticDescriptorsPerReference()
{
return CreateDiagnosticDescriptorsPerReference(_lazyHostDiagnosticAnalyzersPerReferenceMap.Value, projectOpt: null);
}
///
/// Create identity and s map for given
///
public ImmutableDictionary> CreateDiagnosticDescriptorsPerReference(Project project)
{
return CreateDiagnosticDescriptorsPerReference(CreateDiagnosticAnalyzersPerReference(project), project);
}
///
/// Create identity and s map for given that
/// includes both host and project analyzers
///
public ImmutableDictionary> CreateDiagnosticAnalyzersPerReference(Project project)
{
var hostAnalyzerReferences = GetHostDiagnosticAnalyzersPerReference(project.Language);
var projectAnalyzerReferences = CreateProjectDiagnosticAnalyzersPerReference(project);
return MergeDiagnosticAnalyzerMap(hostAnalyzerReferences, projectAnalyzerReferences);
}
///
/// Create identity and s map for given that
/// has only project analyzers
///
public ImmutableDictionary> CreateProjectDiagnosticAnalyzersPerReference(Project project)
{
return CreateDiagnosticAnalyzersPerReferenceMap(CreateProjectAnalyzerReferencesMap(project), project.Language);
}
///
/// Check whether given belong to compiler diagnostic analyzer
///
public bool IsCompilerDiagnostic(string language, DiagnosticData diagnostic)
{
var map = GetHostDiagnosticAnalyzersPerReference(language);
if (_compilerDiagnosticAnalyzerMap.TryGetValue(language, out var compilerAnalyzer) &&
_compilerDiagnosticAnalyzerDescriptorMap.TryGetValue(compilerAnalyzer, out var idMap) &&
idMap.Contains(diagnostic.Id))
{
return true;
}
return false;
}
///
/// Return compiler for the given language.
///
public DiagnosticAnalyzer GetCompilerDiagnosticAnalyzer(string language)
{
var map = GetHostDiagnosticAnalyzersPerReference(language);
if (_compilerDiagnosticAnalyzerMap.TryGetValue(language, out var compilerAnalyzer))
{
return compilerAnalyzer;
}
return null;
}
///
/// Check whether given is compiler analyzer for the language or not.
///
public bool IsCompilerDiagnosticAnalyzer(string language, DiagnosticAnalyzer analyzer)
{
var map = GetHostDiagnosticAnalyzersPerReference(language);
return _compilerDiagnosticAnalyzerMap.TryGetValue(language, out var compilerAnalyzer) && compilerAnalyzer == analyzer;
}
///
/// Get Name of Package (vsix) which Host is from.
///
public string GetDiagnosticAnalyzerPackageName(string language, DiagnosticAnalyzer analyzer)
{
var map = GetHostDiagnosticAnalyzersPerReference(language);
if (_hostDiagnosticAnalyzerPackageNameMap.TryGetValue(analyzer, out var name))
{
return name;
}
return null;
}
private ImmutableDictionary CreateProjectAnalyzerReferencesMap(Project project)
{
return CreateAnalyzerReferencesMap(project.AnalyzerReferences.Where(CheckAnalyzerReferenceIdentity));
}
private ImmutableDictionary> CreateDiagnosticDescriptorsPerReference(
ImmutableDictionary> analyzersMap,
Project projectOpt)
{
var builder = ImmutableDictionary.CreateBuilder>();
foreach (var kv in analyzersMap)
{
var referenceId = kv.Key;
var analyzers = kv.Value;
var descriptors = ImmutableArray.CreateBuilder();
foreach (var analyzer in analyzers)
{
// given map should be in good shape. no duplication. no null and etc
descriptors.AddRange(GetDiagnosticDescriptors(analyzer));
}
// there can't be duplication since _hostAnalyzerReferenceMap is already de-duplicated.
builder.Add(referenceId, descriptors.ToImmutable());
}
return builder.ToImmutable();
}
private ImmutableDictionary> CreateHostDiagnosticAnalyzersAndBuildMap(string language)
{
Contract.ThrowIfNull(language);
var nameMap = CreateAnalyzerPathToPackageNameMap();
var builder = ImmutableDictionary.CreateBuilder>();
foreach (var kv in _hostAnalyzerReferencesMap.Value)
{
var referenceIdentity = kv.Key;
var reference = kv.Value;
var analyzers = reference.GetAnalyzers(language);
if (analyzers.Length == 0)
{
continue;
}
UpdateCompilerAnalyzerMapIfNeeded(language, analyzers);
UpdateDiagnosticAnalyzerToPackageNameMap(nameMap, reference, analyzers);
// there can't be duplication since _hostAnalyzerReferenceMap is already de-duplicated.
builder.Add(referenceIdentity, analyzers);
}
return builder.ToImmutable();
}
private void UpdateDiagnosticAnalyzerToPackageNameMap(
ImmutableDictionary nameMap,
AnalyzerReference reference,
ImmutableArray analyzers)
{
var fileReference = reference as AnalyzerFileReference;
if (fileReference == null)
{
return;
}
if (!nameMap.TryGetValue(fileReference.FullPath, out var name))
{
return;
}
foreach (var analyzer in analyzers)
{
ImmutableInterlocked.GetOrAdd(ref _hostDiagnosticAnalyzerPackageNameMap, analyzer, name);
}
}
private ImmutableDictionary CreateAnalyzerPathToPackageNameMap()
{
var builder = ImmutableDictionary.CreateBuilder(StringComparer.OrdinalIgnoreCase);
foreach (var package in _hostDiagnosticAnalyzerPackages.Value)
{
if (string.IsNullOrEmpty(package.Name))
{
continue;
}
foreach (var assembly in package.Assemblies)
{
if (!builder.ContainsKey(assembly))
{
builder.Add(assembly, package.Name);
}
}
}
return builder.ToImmutable();
}
private void UpdateCompilerAnalyzerMapIfNeeded(string language, ImmutableArray analyzers)
{
if (_compilerDiagnosticAnalyzerMap.ContainsKey(language))
{
return;
}
foreach (var analyzer in analyzers)
{
if (analyzer.IsCompilerAnalyzer())
{
ImmutableInterlocked.GetOrAdd(ref _compilerDiagnosticAnalyzerDescriptorMap, analyzer, a => new HashSet(GetDiagnosticDescriptors(a).Select(d => d.Id)));
ImmutableInterlocked.GetOrAdd(ref _compilerDiagnosticAnalyzerMap, language, analyzer);
return;
}
}
}
private bool CheckAnalyzerReferenceIdentity(AnalyzerReference reference)
{
if (reference == null)
{
return false;
}
return !_hostAnalyzerReferencesMap.Value.ContainsKey(reference.Id);
}
private static ImmutableDictionary> CreateDiagnosticAnalyzersPerReferenceMap(
IDictionary analyzerReferencesMap, string languageOpt = null)
{
var builder = ImmutableDictionary.CreateBuilder>();
foreach (var reference in analyzerReferencesMap)
{
var analyzers = languageOpt == null ? reference.Value.GetAnalyzersForAllLanguages() : reference.Value.GetAnalyzers(languageOpt);
if (analyzers.Length == 0)
{
continue;
}
// input "analyzerReferencesMap" is a dictionary, so there will be no duplication here.
builder.Add(reference.Key, analyzers.WhereNotNull().ToImmutableArray());
}
return builder.ToImmutable();
}
private static ImmutableDictionary CreateAnalyzerReferencesMap(IEnumerable analyzerReferences)
{
var builder = ImmutableDictionary.CreateBuilder();
foreach (var reference in analyzerReferences)
{
var key = reference.Id;
// filter out duplicated analyzer reference
if (builder.ContainsKey(key))
{
continue;
}
builder.Add(key, reference);
}
return builder.ToImmutable();
}
private static ImmutableArray CreateAnalyzerReferencesFromPackages(
ImmutableArray analyzerPackages,
HostAnalyzerReferenceDiagnosticReporter reporter,
IAnalyzerAssemblyLoader hostAnalyzerAssemblyLoader)
{
if (analyzerPackages.IsEmpty)
{
return ImmutableArray.Empty;
}
Contract.ThrowIfNull(hostAnalyzerAssemblyLoader);
var analyzerAssemblies = analyzerPackages.SelectMany(p => p.Assemblies);
var builder = ImmutableArray.CreateBuilder();
foreach (var analyzerAssembly in analyzerAssemblies.Distinct(StringComparer.OrdinalIgnoreCase))
{
var reference = new AnalyzerFileReference(analyzerAssembly, hostAnalyzerAssemblyLoader);
reference.AnalyzerLoadFailed += reporter.OnAnalyzerLoadFailed;
builder.Add(reference);
}
var references = builder.ToImmutable();
DiagnosticAnalyzerLogger.LogWorkspaceAnalyzers(references);
return references;
}
private static ImmutableDictionary> MergeDiagnosticAnalyzerMap(
ImmutableDictionary> map1, ImmutableDictionary> map2)
{
var current = map1;
var seen = new HashSet(map1.Values.SelectMany(v => v));
foreach (var kv in map2)
{
var referenceIdentity = kv.Key;
var analyzers = kv.Value;
if (map1.ContainsKey(referenceIdentity))
{
continue;
}
current = current.Add(referenceIdentity, analyzers.Where(a => seen.Add(a)).ToImmutableArray());
}
return current;
}
private class HostAnalyzerReferenceDiagnosticReporter
{
private readonly AbstractHostDiagnosticUpdateSource _hostUpdateSource;
private readonly PrimaryWorkspace _primaryWorkspace;
public HostAnalyzerReferenceDiagnosticReporter(AbstractHostDiagnosticUpdateSource hostUpdateSource, PrimaryWorkspace primaryWorkspace)
{
_hostUpdateSource = hostUpdateSource;
_primaryWorkspace = primaryWorkspace;
}
public void OnAnalyzerLoadFailed(object sender, AnalyzerLoadFailureEventArgs e)
{
var reference = sender as AnalyzerFileReference;
if (reference == null)
{
return;
}
var diagnostic = AnalyzerHelper.CreateAnalyzerLoadFailureDiagnostic(reference.FullPath, e);
// diagnostic from host analyzer can never go away
var args = DiagnosticsUpdatedArgs.DiagnosticsCreated(
id: Tuple.Create(this, reference.FullPath, e.ErrorCode, e.TypeName),
workspace: _primaryWorkspace.Workspace,
solution: null,
projectId: null,
documentId: null,
diagnostics: ImmutableArray.Create(diagnostic));
// this can be null in test. but in product code, this should never be null.
_hostUpdateSource?.RaiseDiagnosticsUpdated(args);
}
}
}
}