File: Features\DecompiledSource\AssemblyResolver.cs
Web Access
Project: src\src\LanguageServer\Protocol\Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol)
// 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 disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Threading.Tasks;
using ICSharpCode.Decompiler.Metadata;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.DecompiledSource
{
    internal class AssemblyResolver : IAssemblyResolver
    {
        private static readonly Dictionary<MetadataReference, (string fileName, ImmutableArray<byte> image)> _inMemoryImagesForTesting = [];
 
        private readonly Compilation _parentCompilation;
        private readonly Dictionary<string, List<IAssemblySymbol>> _cache = [];
        private readonly StringBuilder _logger;
 
        public AssemblyResolver(Compilation parentCompilation, StringBuilder logger)
        {
            _parentCompilation = parentCompilation;
            _logger = logger;
            BuildReferenceCache();
            Log(FeaturesResources._0_items_in_cache, _cache.Count);
 
            void BuildReferenceCache()
            {
                foreach (var reference in _parentCompilation.GetReferencedAssemblySymbols())
                {
                    if (!_cache.TryGetValue(reference.Identity.Name, out var list))
                    {
                        list = [];
                        _cache.Add(reference.Identity.Name, list);
                    }
 
                    list.Add(reference);
                }
            }
        }
 
        public Task<PEFile> ResolveAsync(IAssemblyReference name)
        {
            return Task.FromResult(Resolve(name));
        }
 
        public Task<PEFile> ResolveModuleAsync(PEFile mainModule, string moduleName)
        {
            return Task.FromResult(ResolveModule(mainModule, moduleName));
        }
 
        [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Could be non-static if instance data is accessed")]
        public PEFile TryResolve(MetadataReference metadataReference, PEStreamOptions streamOptions)
        {
            if (_inMemoryImagesForTesting.TryGetValue(metadataReference, out var pair))
            {
                return new PEFile(pair.fileName, new MemoryStream(pair.image.ToArray()), streamOptions);
            }
 
            return null;
        }
 
        public PEFile Resolve(IAssemblyReference name)
        {
            Log("------------------");
            Log(FeaturesResources.Resolve_0, name.FullName);
 
            // First, find the correct list of assemblies by name
            if (!_cache.TryGetValue(name.Name, out var assemblies))
            {
                Log(FeaturesResources.Could_not_find_by_name_0, name.FullName);
                return null;
            }
 
            // If we have only one assembly available, just use it.
            // This is necessary, because in most cases there is only one assembly,
            // but still might have a version different from what the decompiler asks for.
            if (assemblies.Count == 1)
            {
                Log(FeaturesResources.Found_single_assembly_0, assemblies[0]);
                if (assemblies[0].Identity.Version != name.Version)
                {
                    Log(FeaturesResources.WARN_Version_mismatch_Expected_0_Got_1, name.Version, assemblies[0].Identity.Version);
                }
 
                return MakePEFile(assemblies[0]);
            }
 
            // There are multiple assemblies
            Log(FeaturesResources.Found_0_assemblies_for_1, assemblies.Count, name.Name);
 
            // Get an exact match or highest version match from the list
            IAssemblySymbol highestVersion = null;
            IAssemblySymbol exactMatch = null;
 
            var publicKeyTokenOfName = name.PublicKeyToken ?? [];
 
            foreach (var assembly in assemblies)
            {
                Log(assembly.Identity.GetDisplayName());
                var version = assembly.Identity.Version;
                var publicKeyToken = assembly.Identity.PublicKey;
                if (version == name.Version && publicKeyToken.SequenceEqual(publicKeyTokenOfName))
                {
                    exactMatch = assembly;
                    Log(FeaturesResources.Found_exact_match_0, assembly);
                }
                else if (highestVersion == null || highestVersion.Identity.Version < version)
                {
                    highestVersion = assembly;
                    Log(FeaturesResources.Found_higher_version_match_0, assembly);
                }
            }
 
            var chosen = exactMatch ?? highestVersion;
            Log(FeaturesResources.Chosen_version_0, chosen);
            return MakePEFile(chosen);
 
            PEFile MakePEFile(IAssemblySymbol assembly)
            {
                // reference assemblies should be fine here, we only need the metadata of references.
                var reference = _parentCompilation.GetMetadataReference(assembly);
                Log(FeaturesResources.Load_from_0, reference.Display);
 
                var result = TryResolve(reference, PEStreamOptions.PrefetchMetadata);
                if (result is not null)
                {
                    return result;
                }
 
                if (File.Exists(reference.Display))
                {
                    return new PEFile(reference.Display, PEStreamOptions.PrefetchMetadata);
                }
 
                return null;
            }
        }
 
        public PEFile ResolveModule(PEFile mainModule, string moduleName)
        {
            Log("-------------");
            Log(FeaturesResources.Resolve_module_0_of_1, moduleName, mainModule.FullName);
 
            // Primitive implementation to support multi-module assemblies
            // where all modules are located next to the main module.
            var baseDirectory = Path.GetDirectoryName(mainModule.FileName);
            var moduleFileName = Path.Combine(baseDirectory, moduleName);
            if (!File.Exists(moduleFileName))
            {
                Log(FeaturesResources.Module_not_found);
                return null;
            }
 
            Log(FeaturesResources.Load_from_0, moduleFileName);
            return new PEFile(moduleFileName, PEStreamOptions.PrefetchMetadata);
        }
 
        private void Log(string format, params object[] args)
            => _logger.AppendFormat(format + Environment.NewLine, args);
 
        internal static class TestAccessor
        {
            public static void AddInMemoryImage(MetadataReference reference, string fileName, ImmutableArray<byte> image)
            {
                Contract.ThrowIfNull(fileName);
                _inMemoryImagesForTesting.Add(reference, (fileName, image));
            }
 
            public static bool ContainsInMemoryImage(MetadataReference reference)
            {
                return _inMemoryImagesForTesting.ContainsKey(reference);
            }
 
            public static void ClearInMemoryImages()
            {
                _inMemoryImagesForTesting.Clear();
            }
        }
    }
}