File: src\Compilers\Core\Portable\DiagnosticAnalyzer\AnalyzerAssemblyLoader.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis.ErrorReporting;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis
{
    internal interface IAnalyzerAssemblyLoaderInternal : IAnalyzerAssemblyLoader, IDisposable
    {
        /// <summary>
        /// Is this an <see cref="Assembly"/> that the loader considers to be part of the hosting 
        /// process. Either part of the compiler itself or the process hosting the compiler.
        /// </summary>
        bool IsHostAssembly(Assembly assembly);
 
        /// <summary>
        /// For a given <see cref="AssemblyName"/> return the location it was originally added 
        /// from. This will return null for any value that was not directly added through the 
        /// loader.
        /// </summary>
        string? GetOriginalDependencyLocation(AssemblyName assembly);
    }
 
    /// <summary>
    /// The base implementation for <see cref="IAnalyzerAssemblyLoader"/>. This type provides caching and tracking of inputs given
    /// to <see cref="AddDependencyLocation(string)"/>.
    /// </summary>
    /// <remarks>
    /// This type generally assumes that files on disk aren't changing, since it ensure that two calls to <see cref="LoadFromPath(string)"/>
    /// will always return the same thing, per that interface's contract.
    /// </remarks>
    internal abstract partial class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoaderInternal
    {
        private readonly object _guard = new();
 
        /// <summary>
        /// Set of analyzer dependencies original full paths to the data calculated for that path
        /// </summary>
        /// <remarks>
        /// Access must be guarded by <see cref="_guard"/>
        /// </remarks>
        private readonly Dictionary<string, (AssemblyName? AssemblyName, string RealAssemblyPath)?> _analyzerAssemblyInfoMap = new();
 
        /// <summary>
        /// Mapping of analyzer dependency original full path and culture to the real satellite
        /// assembly path. If the satellite assembly doesn't exist for the original analyzer and 
        /// culture, the real path value stored will be null.
        /// </summary>
        /// <remarks>
        /// Access must be guarded by <see cref="_guard"/>
        /// </remarks>
        private readonly Dictionary<(string OriginalAnalyzerPath, CultureInfo CultureInfo), string?> _analyzerSatelliteAssemblyRealPaths = new();
 
        /// <summary>
        /// Maps analyzer dependency simple names to the set of original full paths it was loaded from. This _only_ 
        /// tracks the paths provided to the analyzer as it's a place to look for indirect loads. 
        /// </summary>
        /// <remarks>
        /// Access must be guarded by <see cref="_guard"/>
        /// </remarks>
        private readonly Dictionary<string, ImmutableHashSet<string>> _knownAssemblyPathsBySimpleName = new(StringComparer.OrdinalIgnoreCase);
 
        /// <summary>
        /// A collection of <see cref="IAnalyzerAssemblyResolver"/>s that can be used to override the assembly resolution process.
        /// </summary>
        /// <remarks>
        /// When multiple resolvers are present they are consulted in-order, with the first resolver to return a non-null
        /// <see cref="Assembly"/> winning.</remarks>
        private readonly ImmutableArray<IAnalyzerAssemblyResolver> _externalResolvers;
 
        /// <summary>
        /// Whether or not we're disposed.  Once disposed, all functionality on this type should throw.
        /// </summary>
        private bool _isDisposed;
 
        /// <summary>
        /// The implementation needs to load an <see cref="Assembly"/> with the specified <see cref="AssemblyName"/>. The
        /// <paramref name="assemblyOriginalPath"/> parameter is the original path. It may be different than
        /// <see cref="AssemblyName.CodeBase"/> as that is empty on .NET Core.
        /// </summary>
        /// <remarks>
        /// This method should return an <see cref="Assembly"/> instance or throw.
        /// </remarks>
        private partial Assembly Load(AssemblyName assemblyName, string assemblyOriginalPath);
 
        /// <summary>
        /// Determines if the <paramref name="candidateName"/> satisfies the request for 
        /// <paramref name="requestedName"/>. This is partial'd out as each runtime has a different 
        /// definition of matching name.
        /// </summary>
        private partial bool IsMatch(AssemblyName requestedName, AssemblyName candidateName);
 
        private void CheckIfDisposed()
        {
#if NET
            ObjectDisposedException.ThrowIf(_isDisposed, this);
#else
            if (_isDisposed)
                throw new ObjectDisposedException(this.GetType().FullName);
#endif
        }
 
        public void Dispose()
        {
            if (_isDisposed)
                return;
 
            _isDisposed = true;
            DisposeWorker();
        }
 
        private partial void DisposeWorker();
 
        internal bool IsAnalyzerDependencyPath(string fullPath)
        {
            CheckIfDisposed();
 
            lock (_guard)
            {
                return _analyzerAssemblyInfoMap.ContainsKey(fullPath);
            }
        }
 
        public void AddDependencyLocation(string fullPath)
        {
            CheckIfDisposed();
 
            CompilerPathUtilities.RequireAbsolutePath(fullPath, nameof(fullPath));
            string simpleName = PathUtilities.GetFileName(fullPath, includeExtension: false);
 
            lock (_guard)
            {
                if (!_knownAssemblyPathsBySimpleName.TryGetValue(simpleName, out var paths))
                {
                    paths = ImmutableHashSet.Create(PathUtilities.Comparer, fullPath);
                    _knownAssemblyPathsBySimpleName.Add(simpleName, paths);
                }
                else
                {
                    _knownAssemblyPathsBySimpleName[simpleName] = paths.Add(fullPath);
                }
 
                // This type assumes the file system is static for the duration of the
                // it's instance. Repeated calls to this method, even if the underlying 
                // file system contents, should reuse the results of the first call.
                _ = _analyzerAssemblyInfoMap.TryAdd(fullPath, null);
            }
        }
 
        public Assembly LoadFromPath(string originalAnalyzerPath)
        {
            CheckIfDisposed();
 
            CompilerPathUtilities.RequireAbsolutePath(originalAnalyzerPath, nameof(originalAnalyzerPath));
 
            (AssemblyName? assemblyName, _) = GetAssemblyInfoForPath(originalAnalyzerPath);
 
            // Not a managed assembly, nothing else to do
            if (assemblyName is null)
            {
                throw new ArgumentException($"Not a valid assembly: {originalAnalyzerPath}");
            }
 
            try
            {
                return Load(assemblyName, originalAnalyzerPath);
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException($"Unable to load {assemblyName.Name}", ex);
            }
        }
 
        /// <summary>
        /// Get the <see cref="AssemblyName"/> and the path it should be loaded from for the given original 
        /// analyzer path
        /// </summary>
        /// <remarks>
        /// This is used in the implementation of the loader instead of <see cref="AssemblyName.GetAssemblyName(string)"/>
        /// because we only want information for registered paths. Using unregistered paths inside the
        /// implementation should result in errors.
        /// </remarks>
        protected (AssemblyName? AssemblyName, string RealAssemblyPath) GetAssemblyInfoForPath(string originalAnalyzerPath)
        {
            CheckIfDisposed();
 
            lock (_guard)
            {
                if (!_analyzerAssemblyInfoMap.TryGetValue(originalAnalyzerPath, out var tuple))
                {
                    throw new InvalidOperationException();
                }
 
                if (tuple is { } info)
                {
                    return info;
                }
            }
 
            string realPath = PreparePathToLoad(originalAnalyzerPath);
            AssemblyName? assemblyName;
            try
            {
                assemblyName = AssemblyName.GetAssemblyName(realPath);
            }
            catch
            {
                // The above can fail when the assembly doesn't exist because it's corrupted, 
                // doesn't exist on disk, or is a native DLL. Those failures are handled when 
                // the actual load is attempted. Just record the failure now.
                assemblyName = null;
            }
 
            lock (_guard)
            {
                _analyzerAssemblyInfoMap[originalAnalyzerPath] = (assemblyName, realPath);
            }
 
            return (assemblyName, realPath);
        }
 
        /// <summary>
        /// Get the path a satellite assembly should be loaded from for the given original 
        /// analyzer path and culture
        /// </summary>
        /// <remarks>
        /// This is used during assembly resolve for satellite assemblies to determine the
        /// path from where the satellite assembly should be loaded for the specified culture.
        /// This method calls <see cref="PrepareSatelliteAssemblyToLoad"/> to ensure this path
        /// contains the satellite assembly.
        /// </remarks>
        internal string? GetRealSatelliteLoadPath(string originalAnalyzerPath, CultureInfo cultureInfo)
        {
            CheckIfDisposed();
 
            string? realSatelliteAssemblyPath = null;
 
            lock (_guard)
            {
                if (_analyzerSatelliteAssemblyRealPaths.TryGetValue((originalAnalyzerPath, cultureInfo), out realSatelliteAssemblyPath))
                {
                    return realSatelliteAssemblyPath;
                }
            }
 
            var actualCultureName = getSatelliteCultureName(originalAnalyzerPath, cultureInfo);
            if (actualCultureName != null)
            {
                realSatelliteAssemblyPath = PrepareSatelliteAssemblyToLoad(originalAnalyzerPath, actualCultureName);
            }
 
            lock (_guard)
            {
                _analyzerSatelliteAssemblyRealPaths[(originalAnalyzerPath, cultureInfo)] = realSatelliteAssemblyPath;
            }
 
            return realSatelliteAssemblyPath;
 
            // Discover the most specific culture name to use for the specified analyzer path and culture
            static string? getSatelliteCultureName(string originalAnalyzerPath, CultureInfo cultureInfo)
            {
                var path = Path.GetDirectoryName(originalAnalyzerPath)!;
                var resourceFileName = GetSatelliteFileName(Path.GetFileName(originalAnalyzerPath));
 
                while (cultureInfo != CultureInfo.InvariantCulture)
                {
                    var resourceFilePath = Path.Combine(path, cultureInfo.Name, resourceFileName);
 
                    if (File.Exists(resourceFilePath))
                    {
                        return cultureInfo.Name;
                    }
 
                    cultureInfo = cultureInfo.Parent;
                }
 
                return null;
            }
        }
 
        public string? GetOriginalDependencyLocation(AssemblyName assemblyName)
        {
            CheckIfDisposed();
 
            return GetBestPath(assemblyName).BestOriginalPath;
        }
        /// <summary>
        /// Return the best (original, real) path information for loading an assembly with the specified <see cref="AssemblyName"/>.
        /// </summary>
        protected (string? BestOriginalPath, string? BestRealPath) GetBestPath(AssemblyName requestedName)
        {
            CheckIfDisposed();
 
            if (requestedName.Name is null)
            {
                return (null, null);
            }
 
            ImmutableHashSet<string>? paths;
            lock (_guard)
            {
                if (!_knownAssemblyPathsBySimpleName.TryGetValue(requestedName.Name, out paths))
                {
                    return (null, null);
                }
            }
 
            // Sort the candidate paths by ordinal, to ensure determinism with the same inputs if you were to have
            // multiple assemblies providing the same version.
            string? bestRealPath = null;
            string? bestOriginalPath = null;
            AssemblyName? bestName = null;
            foreach (var candidateOriginalPath in paths.OrderBy(StringComparer.Ordinal))
            {
                (AssemblyName? candidateName, string candidateRealPath) = GetAssemblyInfoForPath(candidateOriginalPath);
                if (candidateName is null)
                {
                    continue;
                }
 
                if (IsMatch(requestedName, candidateName))
                {
                    if (candidateName.Version == requestedName.Version)
                    {
                        return (candidateOriginalPath, candidateRealPath);
                    }
 
                    if (bestName is null || candidateName.Version > bestName.Version)
                    {
                        bestOriginalPath = candidateOriginalPath;
                        bestRealPath = candidateRealPath;
                        bestName = candidateName;
                    }
                }
            }
 
            return (bestOriginalPath, bestRealPath);
        }
 
        protected static string GetSatelliteFileName(string assemblyFileName) =>
            Path.ChangeExtension(assemblyFileName, ".resources.dll");
 
        /// <summary>
        /// When overridden in a derived class, allows substituting an assembly path after we've
        /// identified the context to load an assembly in, but before the assembly is actually
        /// loaded from disk. This is used to substitute out the original path with the shadow-copied version.
        /// </summary>
        protected abstract string PreparePathToLoad(string assemblyFilePath);
 
        /// <summary>
        /// When overridden in a derived class, allows substituting a satellite assembly path after we've
        /// identified the context to load a satellite assembly in, but before the satellite assembly is actually
        /// loaded from disk. This is used to substitute out the original path with the shadow-copied version.
        /// </summary>
        protected abstract string PrepareSatelliteAssemblyToLoad(string assemblyFilePath, string cultureName);
 
        /// <summary>
        /// When <see cref="PreparePathToLoad(string)"/> is overridden this returns the most recent
        /// real path calculated for the <paramref name="originalFullPath"/>
        /// </summary>
        internal string GetRealAnalyzerLoadPath(string originalFullPath)
        {
            CheckIfDisposed();
 
            lock (_guard)
            {
                if (!_analyzerAssemblyInfoMap.TryGetValue(originalFullPath, out var tuple))
                {
                    throw new InvalidOperationException($"Invalid original path: {originalFullPath}");
                }
 
                return tuple is { } value ? value.RealAssemblyPath : originalFullPath;
            }
        }
 
        internal (string OriginalAssemblyPath, string RealAssemblyPath)[] GetPathMapSnapshot()
        {
            CheckIfDisposed();
 
            lock (_guard)
            {
                return _analyzerAssemblyInfoMap
                    .Select(x => (x.Key, x.Value?.RealAssemblyPath ?? ""))
                    .OrderBy(x => x.Key)
                    .ToArray();
            }
        }
 
        /// <summary>
        /// Iterates the <see cref="_externalResolvers"/> if any, to see if any of them can resolve
        /// the given <see cref="AssemblyName"/> to an <see cref="Assembly"/>.
        /// </summary>
        /// <param name="assemblyName">The name of the assembly to resolve</param>
        /// <returns>An <see langword="assembly"/> if one of the resolvers is successful, or <see langword="null"/></returns>
        internal Assembly? ResolveAssemblyExternally(AssemblyName assemblyName)
        {
            CheckIfDisposed();
 
            if (!_externalResolvers.IsDefaultOrEmpty)
            {
                foreach (var resolver in _externalResolvers)
                {
                    try
                    {
                        if (resolver.ResolveAssembly(assemblyName) is { } resolvedAssembly)
                        {
                            return resolvedAssembly;
                        }
                    }
                    catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.Diagnostic))
                    {
                        // Ignore if the external resolver throws
                    }
                }
            }
            return null;
        }
    }
}