|
// 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.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Threading;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Diagnostics
{
/// <summary>
/// Represents analyzers stored in an analyzer assembly file.
/// </summary>
/// <remarks>
/// Analyzer are read from the file, owned by the reference, and doesn't change
/// since the reference is accessed until the reference object is garbage collected.
///
/// If you need to manage the lifetime of the analyzer reference (and the file stream) explicitly use <see cref="AnalyzerImageReference"/>.
/// </remarks>
public sealed class AnalyzerFileReference : AnalyzerReference, IEquatable<AnalyzerReference>
{
private delegate ImmutableArray<string> AttributeLanguagesFunc(PEModule module, CustomAttributeHandle attribute);
public override string FullPath { get; }
private readonly IAnalyzerAssemblyLoader _assemblyLoader;
private readonly Extensions<DiagnosticAnalyzer> _diagnosticAnalyzers;
private readonly Extensions<ISourceGenerator> _generators;
private string? _lazyDisplay;
private object? _lazyIdentity;
private Assembly? _lazyAssembly;
public event EventHandler<AnalyzerLoadFailureEventArgs>? AnalyzerLoadFailed;
/// <summary>
/// Creates an AnalyzerFileReference with the given <paramref name="fullPath"/> and <paramref name="assemblyLoader"/>.
/// </summary>
/// <param name="fullPath">Full path of the analyzer assembly.</param>
/// <param name="assemblyLoader">Loader for obtaining the <see cref="Assembly"/> from the <paramref name="fullPath"/></param>
public AnalyzerFileReference(string fullPath, IAnalyzerAssemblyLoader assemblyLoader)
{
CompilerPathUtilities.RequireAbsolutePath(fullPath, nameof(fullPath));
FullPath = fullPath;
_assemblyLoader = assemblyLoader ?? throw new ArgumentNullException(nameof(assemblyLoader));
_diagnosticAnalyzers = new(this, typeof(DiagnosticAnalyzerAttribute), GetDiagnosticsAnalyzerSupportedLanguages, allowNetFramework: true);
_generators = new(this, typeof(GeneratorAttribute), GetGeneratorSupportedLanguages, allowNetFramework: false, coerceFunction: CoerceGeneratorType);
// Note this analyzer full path as a dependency location, so that the analyzer loader
// can correctly load analyzer dependencies.
assemblyLoader.AddDependencyLocation(fullPath);
}
public IAnalyzerAssemblyLoader AssemblyLoader => _assemblyLoader;
public override bool Equals(object? obj)
=> Equals(obj as AnalyzerFileReference);
public bool Equals(AnalyzerFileReference? other)
{
if (ReferenceEquals(this, other))
{
return true;
}
return other is object &&
ReferenceEquals(_assemblyLoader, other._assemblyLoader) &&
FullPath == other.FullPath;
}
// legacy, for backwards compat:
public bool Equals(AnalyzerReference? other)
{
if (ReferenceEquals(this, other))
{
return true;
}
if (other is null)
{
return false;
}
if (other is AnalyzerFileReference fileReference)
{
return Equals(fileReference);
}
return FullPath == other.FullPath;
}
public override int GetHashCode()
=> Hash.Combine(RuntimeHelpers.GetHashCode(_assemblyLoader), FullPath.GetHashCode());
public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzersForAllLanguages()
{
// This API returns duplicates of analyzers that support multiple languages.
// We explicitly retain this behaviour to ensure back compat
return _diagnosticAnalyzers.GetExtensionsForAllLanguages(includeDuplicates: true);
}
public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzers(string language)
{
return _diagnosticAnalyzers.GetExtensions(language);
}
public override ImmutableArray<ISourceGenerator> GetGeneratorsForAllLanguages()
{
return _generators.GetExtensionsForAllLanguages(includeDuplicates: false);
}
[Obsolete("Use GetGenerators(string language) or GetGeneratorsForAllLanguages()")]
public override ImmutableArray<ISourceGenerator> GetGenerators()
{
return _generators.GetExtensions(LanguageNames.CSharp);
}
public override ImmutableArray<ISourceGenerator> GetGenerators(string language)
{
return _generators.GetExtensions(language);
}
public override string Display
{
get
{
if (_lazyDisplay == null)
{
InitializeDisplayAndId();
}
return _lazyDisplay;
}
}
public override object Id
{
get
{
if (_lazyIdentity == null)
{
InitializeDisplayAndId();
}
return _lazyIdentity;
}
}
[MemberNotNull(nameof(_lazyIdentity), nameof(_lazyDisplay))]
private void InitializeDisplayAndId()
{
try
{
// AssemblyName.GetAssemblyName(path) is not available on CoreCLR.
// Use our metadata reader to do the equivalent thing.
using var reader = new PEReader(FileUtilities.OpenRead(FullPath));
var metadataReader = reader.GetMetadataReader();
var assemblyIdentity = metadataReader.ReadAssemblyIdentityOrThrow();
_lazyDisplay = assemblyIdentity.Name;
_lazyIdentity = assemblyIdentity;
}
catch
{
_lazyDisplay = FileNameUtilities.GetFileName(FullPath, includeExtension: false);
_lazyIdentity = _lazyDisplay;
}
}
/// <summary>
/// Adds the <see cref="ImmutableArray{T}"/> of <see cref="DiagnosticAnalyzer"/> defined in this assembly reference of given <paramref name="language"/>.
/// </summary>
internal void AddAnalyzers(ImmutableArray<DiagnosticAnalyzer>.Builder builder, string language, Func<DiagnosticAnalyzer, bool>? shouldInclude = null)
{
_diagnosticAnalyzers.AddExtensions(builder, language, shouldInclude);
}
/// <summary>
/// Adds the <see cref="ImmutableArray{T}"/> of <see cref="ISourceGenerator"/> defined in this assembly reference of given <paramref name="language"/>.
/// </summary>
internal void AddGenerators(ImmutableArray<ISourceGenerator>.Builder builder, string language)
{
_generators.AddExtensions(builder, language);
}
private static AnalyzerLoadFailureEventArgs CreateAnalyzerFailedArgs(Exception e, string? typeName = null)
{
// remove all line breaks from the exception message
string message = e.Message.Replace("\r", "").Replace("\n", "");
var errorCode = (typeName != null) ?
AnalyzerLoadFailureEventArgs.FailureErrorCode.UnableToCreateAnalyzer :
AnalyzerLoadFailureEventArgs.FailureErrorCode.UnableToLoadAnalyzer;
return new AnalyzerLoadFailureEventArgs(errorCode, message, e, typeName);
}
internal ImmutableSortedDictionary<string, ImmutableHashSet<string>> GetAnalyzerTypeNameMap()
{
return _diagnosticAnalyzers.GetExtensionTypeNameMap();
}
/// <summary>
/// Opens the analyzer dll with the metadata reader and builds a map of language -> analyzer type names.
/// </summary>
/// <exception cref="BadImageFormatException">The PE image format is invalid.</exception>
/// <exception cref="IOException">IO error reading the metadata.</exception>
[PerformanceSensitive("https://github.com/dotnet/roslyn/issues/30449")]
private static ImmutableSortedDictionary<string, ImmutableHashSet<string>> GetAnalyzerTypeNameMap(string fullPath, Type attributeType, AttributeLanguagesFunc languagesFunc)
{
using var assembly = AssemblyMetadata.CreateFromFile(fullPath);
Dictionary<string, ImmutableHashSet<string>.Builder> typeNameMap = new Dictionary<string, ImmutableHashSet<string>.Builder>(StringComparer.OrdinalIgnoreCase);
foreach (var module in assembly.GetModules())
{
foreach (var typeDefHandle in module.MetadataReader.TypeDefinitions)
{
var typeDef = module.MetadataReader.GetTypeDefinition(typeDefHandle);
var supportedLanguages = GetSupportedLanguages(typeDef, module.Module, attributeType, languagesFunc);
// PERF: avoid calling GetFullyQualifiedTypeName when no supported languages.
if (supportedLanguages.Length > 0)
{
var typeName = GetFullyQualifiedTypeName(typeDef, module.Module);
foreach (var supportedLanguage in supportedLanguages)
{
if (!typeNameMap.TryGetValue(supportedLanguage, out var builder))
{
builder = ImmutableHashSet.CreateBuilder<string>();
typeNameMap.Add(supportedLanguage, builder);
}
builder.Add(typeName);
}
}
}
}
return typeNameMap.ToImmutableSortedDictionary(g => g.Key, g => g.Value.ToImmutable(), StringComparer.OrdinalIgnoreCase);
}
private static ImmutableArray<string> GetSupportedLanguages(TypeDefinition typeDef, PEModule peModule, Type attributeType, AttributeLanguagesFunc languagesFunc)
{
ImmutableArray<string> result = [];
foreach (CustomAttributeHandle customAttrHandle in typeDef.GetCustomAttributes())
{
if (peModule.IsTargetAttribute(customAttrHandle, attributeType.Namespace!, attributeType.Name, ctor: out _))
{
if (languagesFunc(peModule, customAttrHandle) is { } attributeSupportedLanguages)
{
if (result.IsDefaultOrEmpty)
{
result = attributeSupportedLanguages;
}
else
{
// This is a slow path, but only occurs if a single type has multiple
// DiagnosticAnalyzerAttribute instances applied to it.
result = result.AddRange(attributeSupportedLanguages);
}
}
}
}
return result;
}
private static ImmutableArray<string> GetDiagnosticsAnalyzerSupportedLanguages(PEModule peModule, CustomAttributeHandle customAttrHandle)
{
// The DiagnosticAnalyzerAttribute has one constructor, which has a string parameter for the
// first supported language and an array parameter for additional supported languages.
// Parse the argument blob to extract the languages.
BlobReader argsReader = peModule.GetMemoryReaderOrThrow(peModule.GetCustomAttributeValueOrThrow(customAttrHandle));
return ReadLanguagesFromAttribute(ref argsReader);
}
private static ImmutableArray<string> GetGeneratorSupportedLanguages(PEModule peModule, CustomAttributeHandle customAttrHandle)
{
// The GeneratorAttribute has two constructors: one default, and one with a string parameter for the
// first supported language and an array parameter for additional supported languages.
BlobReader argsReader = peModule.GetMemoryReaderOrThrow(peModule.GetCustomAttributeValueOrThrow(customAttrHandle));
if (argsReader.Length == 4)
{
// default ctor
return ImmutableArray.Create(LanguageNames.CSharp);
}
else
{
// Parse the argument blob to extract the languages.
return ReadLanguagesFromAttribute(ref argsReader);
}
}
// https://github.com/dotnet/roslyn/issues/53994 tracks re-enabling nullable and fixing this method
#nullable disable
private static ImmutableArray<string> ReadLanguagesFromAttribute(ref BlobReader argsReader)
{
if (argsReader.Length > 4)
{
// Arguments are present--check prologue.
if (argsReader.ReadByte() == 1 && argsReader.ReadByte() == 0)
{
string firstLanguageName;
if (!PEModule.CrackStringInAttributeValue(out firstLanguageName, ref argsReader))
{
return [];
}
ImmutableArray<string> additionalLanguageNames;
if (PEModule.CrackStringArrayInAttributeValue(out additionalLanguageNames, ref argsReader))
{
if (additionalLanguageNames.Length == 0)
{
return [firstLanguageName];
}
return additionalLanguageNames.Insert(0, firstLanguageName);
}
}
}
return [];
}
#nullable enable
private static ISourceGenerator? CoerceGeneratorType(object? generator)
{
if (generator is IIncrementalGenerator incrementalGenerator)
{
return new IncrementalGeneratorWrapper(incrementalGenerator);
}
return null;
}
private static string GetFullyQualifiedTypeName(TypeDefinition typeDef, PEModule peModule)
{
var declaringType = typeDef.GetDeclaringType();
// Non nested type - simply get the full name
if (declaringType.IsNil)
{
return peModule.GetFullNameOrThrow(typeDef.Namespace, typeDef.Name);
}
else
{
var declaringTypeDef = peModule.MetadataReader.GetTypeDefinition(declaringType);
return GetFullyQualifiedTypeName(declaringTypeDef, peModule) + "+" + peModule.MetadataReader.GetString(typeDef.Name);
}
}
private sealed class Extensions<TExtension>
where TExtension : class
{
private readonly AnalyzerFileReference _reference;
private readonly Type _attributeType;
private readonly AttributeLanguagesFunc _languagesFunc;
private readonly bool _allowNetFramework;
private readonly Func<object?, TExtension?>? _coerceFunction;
private ImmutableArray<TExtension> _lazyAllExtensions;
private ImmutableDictionary<string, ImmutableArray<TExtension>> _lazyExtensionsPerLanguage;
private ImmutableSortedDictionary<string, ImmutableHashSet<string>>? _lazyExtensionTypeNameMap;
internal Extensions(AnalyzerFileReference reference, Type attributeType, AttributeLanguagesFunc languagesFunc, bool allowNetFramework, Func<object?, TExtension?>? coerceFunction = null)
{
_reference = reference;
_attributeType = attributeType;
_languagesFunc = languagesFunc;
_allowNetFramework = allowNetFramework;
_coerceFunction = coerceFunction;
_lazyAllExtensions = default;
_lazyExtensionsPerLanguage = ImmutableDictionary<string, ImmutableArray<TExtension>>.Empty;
}
internal ImmutableArray<TExtension> GetExtensionsForAllLanguages(bool includeDuplicates)
{
if (_lazyAllExtensions.IsDefault)
{
ImmutableInterlocked.InterlockedInitialize(ref _lazyAllExtensions, CreateExtensionsForAllLanguages(this, includeDuplicates));
}
return _lazyAllExtensions;
}
private static ImmutableArray<TExtension> CreateExtensionsForAllLanguages(Extensions<TExtension> extensions, bool includeDuplicates)
{
// Get all analyzers in the assembly.
var map = ImmutableSortedDictionary.CreateBuilder<string, ImmutableArray<TExtension>>(StringComparer.OrdinalIgnoreCase);
extensions.AddExtensions(map);
var builder = ImmutableArray.CreateBuilder<TExtension>();
foreach (var analyzers in map.Values)
{
foreach (var analyzer in analyzers)
{
builder.Add(analyzer);
}
}
if (includeDuplicates)
{
return builder.ToImmutable();
}
else
{
return builder.Distinct(ExtTypeComparer.Instance).ToImmutableArray();
}
}
private class ExtTypeComparer : IEqualityComparer<TExtension>
{
public static readonly ExtTypeComparer Instance = new();
public bool Equals(TExtension? x, TExtension? y) => object.Equals(x?.GetType(), y?.GetType());
public int GetHashCode(TExtension obj) => obj.GetType().GetHashCode();
}
internal ImmutableArray<TExtension> GetExtensions(string language)
{
if (string.IsNullOrEmpty(language))
{
throw new ArgumentException("language");
}
return ImmutableInterlocked.GetOrAdd(ref _lazyExtensionsPerLanguage, language, CreateLanguageSpecificExtensions, this);
}
private static ImmutableArray<TExtension> CreateLanguageSpecificExtensions(string language, Extensions<TExtension> extensions)
{
// Get all analyzers in the assembly for the given language.
var builder = ImmutableArray.CreateBuilder<TExtension>();
extensions.AddExtensions(builder, language);
return builder.ToImmutable();
}
internal ImmutableSortedDictionary<string, ImmutableHashSet<string>> GetExtensionTypeNameMap()
{
if (_lazyExtensionTypeNameMap == null)
{
var analyzerTypeNameMap = GetAnalyzerTypeNameMap(_reference.FullPath, _attributeType, _languagesFunc);
Interlocked.CompareExchange(ref _lazyExtensionTypeNameMap, analyzerTypeNameMap, null);
}
return _lazyExtensionTypeNameMap;
}
internal void AddExtensions(ImmutableSortedDictionary<string, ImmutableArray<TExtension>>.Builder builder)
{
ImmutableSortedDictionary<string, ImmutableHashSet<string>> analyzerTypeNameMap;
Assembly analyzerAssembly;
try
{
analyzerTypeNameMap = GetExtensionTypeNameMap();
if (analyzerTypeNameMap.Count == 0)
{
return;
}
analyzerAssembly = _reference.GetAssembly();
if (CheckAssemblyReferencesNewerCompiler(analyzerAssembly))
{
return;
}
}
catch (Exception e)
{
_reference.AnalyzerLoadFailed?.Invoke(_reference, CreateAnalyzerFailedArgs(e));
return;
}
var initialCount = builder.Count;
var reportedError = false;
// Add language specific analyzers.
foreach (var (language, _) in analyzerTypeNameMap)
{
if (language == null)
{
continue;
}
var analyzers = GetLanguageSpecificAnalyzers(analyzerAssembly, analyzerTypeNameMap, language, ref reportedError);
builder.Add(language, analyzers);
}
// If there were types with the attribute but weren't an analyzer, generate a diagnostic.
// If we've reported errors already while trying to instantiate types, don't complain that there are no analyzers.
if (builder.Count == initialCount && !reportedError)
{
_reference.AnalyzerLoadFailed?.Invoke(_reference, new AnalyzerLoadFailureEventArgs(AnalyzerLoadFailureEventArgs.FailureErrorCode.NoAnalyzers, CodeAnalysisResources.NoAnalyzersFound));
}
}
internal void AddExtensions(ImmutableArray<TExtension>.Builder builder, string language, Func<TExtension, bool>? shouldInclude = null)
{
ImmutableSortedDictionary<string, ImmutableHashSet<string>> analyzerTypeNameMap;
Assembly analyzerAssembly;
try
{
analyzerTypeNameMap = GetExtensionTypeNameMap();
// If there are no analyzers, don't load the assembly at all.
if (!analyzerTypeNameMap.ContainsKey(language))
{
return;
}
analyzerAssembly = _reference.GetAssembly();
if (analyzerAssembly == null || CheckAssemblyReferencesNewerCompiler(analyzerAssembly))
{
// This can be null if NoOpAnalyzerAssemblyLoader is used.
return;
}
}
catch (Exception e)
{
_reference.AnalyzerLoadFailed?.Invoke(_reference, CreateAnalyzerFailedArgs(e));
return;
}
var reportedError = false;
// Add language specific analyzers.
var analyzers = GetLanguageSpecificAnalyzers(analyzerAssembly, analyzerTypeNameMap, language, ref reportedError);
var hasAnalyzers = !analyzers.IsEmpty;
if (shouldInclude != null)
{
analyzers = analyzers.WhereAsArray(shouldInclude);
}
builder.AddRange(analyzers);
// If there were types with the attribute but weren't an analyzer, generate a diagnostic.
// If we've reported errors already while trying to instantiate types, don't complain that there are no analyzers.
if (!hasAnalyzers && !reportedError)
{
_reference.AnalyzerLoadFailed?.Invoke(_reference, new AnalyzerLoadFailureEventArgs(AnalyzerLoadFailureEventArgs.FailureErrorCode.NoAnalyzers, CodeAnalysisResources.NoAnalyzersFound));
}
}
bool CheckAssemblyReferencesNewerCompiler(Assembly analyzerAssembly)
{
var runningCompilerAssemblyName = typeof(AnalyzerFileReference).Assembly.GetName();
foreach (var referencedAssemblyName in analyzerAssembly.GetReferencedAssemblies())
{
if (string.Equals(referencedAssemblyName.Name, runningCompilerAssemblyName.Name, StringComparison.OrdinalIgnoreCase)
&& referencedAssemblyName.Version > runningCompilerAssemblyName.Version)
{
// note: we introduce an actual message for this scenario when handling the failed event.
_reference.AnalyzerLoadFailed?.Invoke(_reference, new AnalyzerLoadFailureEventArgs(AnalyzerLoadFailureEventArgs.FailureErrorCode.ReferencesNewerCompiler, message: "") { ReferencedCompilerVersion = referencedAssemblyName.Version });
return true;
}
}
return false;
}
private ImmutableArray<TExtension> GetLanguageSpecificAnalyzers(Assembly analyzerAssembly, ImmutableSortedDictionary<string, ImmutableHashSet<string>> analyzerTypeNameMap, string language, ref bool reportedError)
{
ImmutableHashSet<string>? languageSpecificAnalyzerTypeNames;
if (!analyzerTypeNameMap.TryGetValue(language, out languageSpecificAnalyzerTypeNames))
{
return ImmutableArray<TExtension>.Empty;
}
return this.GetAnalyzersForTypeNames(analyzerAssembly, languageSpecificAnalyzerTypeNames, ref reportedError);
}
private ImmutableArray<TExtension> GetAnalyzersForTypeNames(Assembly analyzerAssembly, ImmutableHashSet<string> analyzerTypeNames, ref bool reportedError)
{
var builder = ArrayBuilder<(string typeName, TExtension analyzer)>.GetInstance();
// Given the type names, get the actual System.Type and try to create an instance of the type through reflection.
// Randomize the order we instantiate analyzers to avoid static constructor/JIT contention, but still return
// the list of analyzers in the order of the sorted type names for deterministic purpose.
foreach (var typeName in shuffle(analyzerTypeNames))
{
Type? type;
try
{
type = analyzerAssembly.GetType(typeName, throwOnError: true, ignoreCase: false);
}
catch (Exception e)
{
_reference.AnalyzerLoadFailed?.Invoke(_reference, CreateAnalyzerFailedArgs(e, typeName));
reportedError = true;
continue;
}
Debug.Assert(type != null);
// check if this references net framework, and issue a diagnostic that this isn't supported
if (!_allowNetFramework)
{
var targetFrameworkAttribute = analyzerAssembly.GetCustomAttribute<TargetFrameworkAttribute>();
if (targetFrameworkAttribute is object && targetFrameworkAttribute.FrameworkName.StartsWith(".NETFramework", StringComparison.OrdinalIgnoreCase))
{
_reference.AnalyzerLoadFailed?.Invoke(_reference, new AnalyzerLoadFailureEventArgs(
AnalyzerLoadFailureEventArgs.FailureErrorCode.ReferencesFramework,
string.Format(CodeAnalysisResources.AssemblyReferencesNetFramework, typeName),
typeNameOpt: typeName));
continue;
}
}
object? typeInstance;
try
{
typeInstance = Activator.CreateInstance(type);
}
catch (Exception e)
{
_reference.AnalyzerLoadFailed?.Invoke(_reference, CreateAnalyzerFailedArgs(e, typeName));
reportedError = true;
continue;
}
TExtension? analyzer = typeInstance as TExtension ?? _coerceFunction?.Invoke(typeInstance);
if (analyzer != null)
{
builder.Add((typeName, analyzer));
}
}
builder.Sort(static (x, y) => string.Compare(x.typeName, y.typeName, StringComparison.OrdinalIgnoreCase));
var analyzers = builder.SelectAsArray(x => x.analyzer);
builder.Free();
return analyzers;
static IEnumerable<string> shuffle(ImmutableHashSet<string> source)
{
var random =
#if NET6_0_OR_GREATER
Random.Shared;
#else
new Random();
#endif
var builder = ArrayBuilder<string>.GetInstance(source.Count);
builder.AddRange(source);
for (var i = builder.Count - 1; i >= 0; i--)
{
var swapIndex = random.Next(i + 1);
yield return builder[swapIndex];
builder[swapIndex] = builder[i];
}
builder.Free();
}
}
}
public Assembly GetAssembly()
{
if (_lazyAssembly == null)
{
_lazyAssembly = _assemblyLoader.LoadFromPath(FullPath);
}
return _lazyAssembly;
}
}
}
|