// 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); } } } }