// 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. #nullable enable 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.Threading; using Microsoft.CodeAnalysis; using SharpGen.Doc; using SharpGenTools.Sdk.Internal; using SharpGenTools.Sdk.Internal.Roslyn; namespace SharpGenTools.Sdk.Extensibility { /// /// Represents analyzers stored in an analyzer assembly file. /// /// /// 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. /// internal sealed class ExtensionFileReference : ExtensionReference, IEquatable { private const string DotDelimiterString = "."; private static readonly string DocProviderNamespace = typeof(IDocProvider).Namespace!; private const string DocProviderName = nameof(IDocProvider); private delegate bool ExtensionPredicate(ModuleMetadata module, InterfaceImplementation interfaceImpl); public override string FullPath { get; } private readonly Extensions _docProviders; private string? _lazyDisplay; private object? _lazyIdentity; private Assembly? _lazyAssembly; public event EventHandler? AnalyzerLoadFailed; /// /// Creates an ExtensionFileReference with the given and . /// /// Full path of the analyzer assembly. /// Loader for obtaining the from the public ExtensionFileReference(string fullPath, ExtensibilityAssemblyLoader assemblyLoader) { Utilities.RequireAbsolutePath(fullPath, nameof(fullPath)); FullPath = fullPath; AssemblyLoader = assemblyLoader ?? throw new ArgumentNullException(nameof(assemblyLoader)); _docProviders = new Extensions(this, IsDocProviderPredicate); // Note this analyzer full path as a dependency location, so that the analyzer loader // can correctly load analyzer dependencies. assemblyLoader.AddDependencyLocation(fullPath); } public ExtensibilityAssemblyLoader AssemblyLoader { get; } public override bool Equals(object? obj) => Equals(obj as ExtensionFileReference); public bool Equals(ExtensionFileReference? other) { if (ReferenceEquals(this, other)) return true; return other != null && ReferenceEquals(AssemblyLoader, other.AssemblyLoader) && FullPath == other.FullPath; } // legacy, for backwards compat: public bool Equals(ExtensionReference? other) { if (ReferenceEquals(this, other)) return true; return other switch { null => false, ExtensionFileReference fileReference => Equals(fileReference), _ => FullPath == other.FullPath }; } public override int GetHashCode() => HashCode.Combine(RuntimeHelpers.GetHashCode(AssemblyLoader), FullPath.GetHashCode()); public override ImmutableArray GetDocumentationProviders() { return _docProviders.GetExtensionsForAllLanguages(); } 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(Utilities.OpenRead(FullPath)); var metadataReader = reader.GetMetadataReader(); var assemblyIdentity = ReadAssemblyIdentityOrThrow(metadataReader); _lazyDisplay = assemblyIdentity.Name; _lazyIdentity = assemblyIdentity; } catch { _lazyDisplay = FileNameUtilities.GetFileName(FullPath, false); _lazyIdentity = _lazyDisplay; } } /// An exception from metadata reader. private static AssemblyIdentity ReadAssemblyIdentityOrThrow(MetadataReader reader) { var assemblyDef = reader.GetAssemblyDefinition(); return CreateAssemblyIdentityOrThrow(reader, assemblyDef.Version, assemblyDef.Flags, assemblyDef.PublicKey, assemblyDef.Name, assemblyDef.Culture, false); } /// An exception from metadata reader. private static AssemblyIdentity CreateAssemblyIdentityOrThrow( MetadataReader reader, Version version, AssemblyFlags flags, BlobHandle publicKey, StringHandle name, StringHandle culture, bool isReference) { string nameStr = reader.GetString(name); var cultureName = culture.IsNil ? null : reader.GetString(culture); var publicKeyOrToken = reader.GetBlobContent(publicKey); bool hasPublicKey; if (isReference) { hasPublicKey = (flags & AssemblyFlags.PublicKey) != 0; } else { // Assembly definitions never contain a public key token, they only can have a full key or nothing, // so the flag AssemblyFlags.PublicKey does not make sense for them and is ignored. // See Ecma-335, Partition II Metadata, 22.2 "Assembly : 0x20". // This also corresponds to the behavior of the native C# compiler and sn.exe tool. hasPublicKey = !publicKeyOrToken.IsEmpty; } if (publicKeyOrToken.IsEmpty) { publicKeyOrToken = default; } return new AssemblyIdentity( nameStr, version, cultureName, publicKeyOrToken, hasPublicKey, (flags & AssemblyFlags.Retargetable) != 0, (AssemblyContentType) ((int) (flags & AssemblyFlags.ContentTypeMask) >> 9)); } /// /// Adds the of defined in this assembly reference. /// internal void AddDocumentationProviders(ImmutableArray.Builder builder) { _docProviders.AddExtensions(builder); } private static ExtensionLoadFailureEventArgs CreateExtensionLoadFailedArgs(Exception e, string? typeNameOpt = null) { // unwrap: e = e as TargetInvocationException ?? e; // remove all line breaks from the exception message string message = e.Message.Replace("\r", string.Empty).Replace("\n", string.Empty); var errorCode = typeNameOpt != null ? ExtensionLoadFailureEventArgs.FailureErrorCode.UnableToCreateExtension : ExtensionLoadFailureEventArgs.FailureErrorCode.UnableToLoadExtension; return new ExtensionLoadFailureEventArgs(errorCode, message, e, typeNameOpt); } /// /// Opens the analyzer dll with the metadata reader and builds a map of language -> analyzer type names. /// /// The PE image format is invalid. /// IO error reading the metadata. private static ImmutableHashSet GetAnalyzerTypeNameMap(string fullPath, ExtensionPredicate extensionPredicate) { using var assembly = AssemblyMetadata.CreateFromFile(fullPath); // This is longer than strictly necessary to avoid thrashing the GC with string allocations // in the call to GetFullyQualifiedTypeNames. Specifically, this checks for the presence of // supported languages prior to creating the type names. return (from module in assembly.GetModules() from typeDefHandle in module.GetMetadataReader().TypeDefinitions let typeDef = module.GetMetadataReader().GetTypeDefinition(typeDefHandle) where GetSupportedLanguages(typeDef, module, extensionPredicate) select GetFullyQualifiedTypeName(typeDef, module)).ToImmutableHashSet(); } private static bool GetSupportedLanguages(TypeDefinition typeDef, ModuleMetadata module, ExtensionPredicate extensionPredicate) => typeDef.GetInterfaceImplementations() .Select(x => module.GetMetadataReader().GetInterfaceImplementation(x)) .Any(customAttrHandle => extensionPredicate(module, customAttrHandle)); private static bool IsDocProviderPredicate(ModuleMetadata module, InterfaceImplementation interfaceImpl) { if (!GetTypeNamespaceAndName(module.GetMetadataReader(), interfaceImpl.Interface, out var namespaceHandle, out var nameHandle)) return false; var comparer = module.GetMetadataReader().StringComparer; return comparer.Equals(nameHandle, DocProviderName) && comparer.Equals(namespaceHandle, DocProviderNamespace); } /// /// Given a token for a type, return the type's name and namespace. Only works for top level types. /// namespaceHandle will be NamespaceDefinitionHandle for defs and StringHandle for refs. /// /// True if the function successfully returns the name and namespace. private static bool GetTypeNamespaceAndName(MetadataReader metadataReader, EntityHandle typeDefOrRef, out StringHandle namespaceHandle, out StringHandle nameHandle) { nameHandle = default; namespaceHandle = default; try { return typeDefOrRef.Kind switch { HandleKind.TypeReference => GetTypeRefNamespaceAndName( (TypeReferenceHandle) typeDefOrRef, ref namespaceHandle, ref nameHandle ), HandleKind.TypeDefinition => GetTypeDefNamespaceAndName( (TypeDefinitionHandle) typeDefOrRef, ref namespaceHandle, ref nameHandle ), _ => false }; } catch (BadImageFormatException) { return false; } bool GetTypeDefNamespaceAndName(TypeDefinitionHandle typeDefHandle, ref StringHandle namespaceHandle, ref StringHandle nameHandle) { var def = metadataReader.GetTypeDefinition(typeDefHandle); if (IsNested(def.Attributes)) { // TODO - Support nested types. return false; } nameHandle = def.Name; namespaceHandle = def.Namespace; return true; static bool IsNested(TypeAttributes flags) => (flags & TypeAttributes.NestedFamANDAssem) != 0; } bool GetTypeRefNamespaceAndName(TypeReferenceHandle typeRefHandle, ref StringHandle namespaceHandle, ref StringHandle nameHandle) { var typeRefRow = metadataReader.GetTypeReference(typeRefHandle); var handleType = typeRefRow.ResolutionScope.Kind; if (handleType == HandleKind.TypeReference || handleType == HandleKind.TypeDefinition) { // TODO - Support nested types. return false; } nameHandle = typeRefRow.Name; namespaceHandle = typeRefRow.Namespace; return true; } } private static string GetFullyQualifiedTypeName(TypeDefinition typeDef, ModuleMetadata module) { var declaringType = typeDef.GetDeclaringType(); // Non nested type - simply get the full name if (declaringType.IsNil) return GetFullNameOrThrow(module, typeDef.Namespace, typeDef.Name); var declaringTypeDef = module.GetMetadataReader().GetTypeDefinition(declaringType); return GetFullyQualifiedTypeName(declaringTypeDef, module) + "+" + module.GetMetadataReader().GetString(typeDef.Name); } /// An exception from metadata reader. private static string GetFullNameOrThrow(ModuleMetadata module, StringHandle namespaceHandle, StringHandle nameHandle) { var attributeTypeName = module.GetMetadataReader().GetString(nameHandle); var attributeTypeNamespaceName = module.GetMetadataReader().GetString(namespaceHandle); return BuildQualifiedName(attributeTypeNamespaceName, attributeTypeName); static string BuildQualifiedName(string qualifier, string name) { Debug.Assert(name != null); return string.IsNullOrEmpty(qualifier) ? name! : string.Concat(qualifier, DotDelimiterString, name); } } private sealed class Extensions where TExtension : class { private readonly ExtensionFileReference _reference; private readonly ExtensionPredicate _extensionPredicate; private ImmutableArray _lazyAllExtensions; private ImmutableHashSet? _lazyExtensionTypeNameMap; internal Extensions(ExtensionFileReference reference, ExtensionPredicate extensionPredicate) { _reference = reference; _extensionPredicate = extensionPredicate; _lazyAllExtensions = default; } internal ImmutableArray GetExtensionsForAllLanguages() { if (_lazyAllExtensions.IsDefault) { ImmutableInterlocked.InterlockedInitialize(ref _lazyAllExtensions, CreateExtensionsForAllLanguages(this)); } return _lazyAllExtensions; } private static ImmutableArray CreateExtensionsForAllLanguages(Extensions extensions) { // Get all analyzers in the assembly. var builder = ImmutableArray.CreateBuilder(); extensions.AddExtensions(builder); return builder.ToImmutable(); } internal ImmutableHashSet GetExtensionTypeNameMap() { if (_lazyExtensionTypeNameMap == null) { var analyzerTypeNameMap = GetAnalyzerTypeNameMap(_reference.FullPath, _extensionPredicate); Interlocked.CompareExchange(ref _lazyExtensionTypeNameMap, analyzerTypeNameMap, null); } return _lazyExtensionTypeNameMap; } internal void AddExtensions(ImmutableArray.Builder builder) { ImmutableHashSet analyzerTypeNameMap; Assembly analyzerAssembly; try { analyzerTypeNameMap = GetExtensionTypeNameMap(); if (analyzerTypeNameMap.Count == 0) { return; } analyzerAssembly = _reference.GetAssembly(); } catch (Exception e) { _reference.AnalyzerLoadFailed?.Invoke(_reference, CreateExtensionLoadFailedArgs(e)); return; } var initialCount = builder.Count; var reportedError = false; GetAnalyzersForTypeNames(analyzerAssembly, analyzerTypeNameMap, builder, ref reportedError); // If there were types implementing TExtension but couldn't be cast to TExtension, generate a diagnostic. // If we've reported errors already while trying to instantiate types, don't complain. if (builder.Count == initialCount && !reportedError) { _reference.AnalyzerLoadFailed?.Invoke( _reference, new ExtensionLoadFailureEventArgs( ExtensionLoadFailureEventArgs.FailureErrorCode.InternalExtensionEntryPointCastError, "Internal extensibility error: extension entry point cannot be casted to a target extension type" ) ); } } private void GetAnalyzersForTypeNames(Assembly analyzerAssembly, IEnumerable analyzerTypeNames, ImmutableArray.Builder builder, ref bool reportedError) { // Given the type names, get the actual System.Type and try to create an instance of the type through reflection. foreach (var typeName in analyzerTypeNames) { Type? type; try { type = analyzerAssembly.GetType(typeName, true, false); } catch (Exception e) { _reference.AnalyzerLoadFailed?.Invoke(_reference, CreateExtensionLoadFailedArgs(e, typeName)); reportedError = true; continue; } Debug.Assert(type != null); TExtension? analyzer; try { analyzer = Activator.CreateInstance(type) as TExtension; } catch (Exception e) { _reference.AnalyzerLoadFailed?.Invoke(_reference, CreateExtensionLoadFailedArgs(e, typeName)); reportedError = true; continue; } if (analyzer != null) { builder.Add(analyzer); } } } } public Assembly GetAssembly() { if (_lazyAssembly == null) { _lazyAssembly = AssemblyLoader.LoadFromPath(FullPath); } return _lazyAssembly; } } }