File: Loader\TypeLoader.cs
Web Access
Project: Microsoft.Build.Framework.csproj (Microsoft.Build.Framework)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
#if NET
using System.Runtime.CompilerServices;
#endif
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Build.Eventing;
using Microsoft.Build.Framework;
using static Microsoft.Build.Shared.XMakeAttributes;

#nullable disable

namespace Microsoft.Build.Shared;

/// <summary>
/// This class is used to load types from their assemblies.
/// </summary>
internal class TypeLoader
{
#if NET && FEATURE_ASSEMBLYLOADCONTEXT
    /// <summary>
    /// AssemblyContextLoader used to load DLLs outside of msbuild.exe directory
    /// </summary>
    private static readonly CoreClrAssemblyLoader s_coreClrAssemblyLoader = new CoreClrAssemblyLoader();
#endif

    /// <summary>
    /// Assembly name that indicates .NET Core/5+ if present as a referenced assembly.
    /// </summary>
    private const string SystemRuntimeAssemblyName = "System.Runtime";

    /// <summary>
    /// NET target moniker name.
    /// </summary>
    private const string DotNetCoreIdentifier = ".NETCore";

    /// <summary>
    /// Assembly custom attribute name.
    /// </summary>
    private const string TargetFrameworkAttributeName = "TargetFrameworkAttribute";

    /// <summary>
    /// Versioning namespace name.
    /// </summary>
    private const string VersioningNamespaceName = "System.Runtime.Versioning";

    /// <summary>
    /// Cache to keep track of the assemblyLoadInfos based on the desired interface.
    /// </summary>
    private static readonly ConcurrentDictionary<Type, ConcurrentDictionary<AssemblyLoadInfo, AssemblyInfoToLoadedTypes>> s_cacheOfLoadedTypesByFilter = new ConcurrentDictionary<Type, ConcurrentDictionary<AssemblyLoadInfo, AssemblyInfoToLoadedTypes>>();

    /// <summary>
    /// Cache to keep track of the assemblyLoadInfos based on the desired interface for assemblies which are to be loaded for reflectionOnlyLoads.
    /// </summary>
    private static readonly ConcurrentDictionary<Type, ConcurrentDictionary<AssemblyLoadInfo, AssemblyInfoToLoadedTypes>> s_cacheOfReflectionOnlyLoadedTypesByFilter = new ConcurrentDictionary<Type, ConcurrentDictionary<AssemblyLoadInfo, AssemblyInfoToLoadedTypes>>();

    /// <summary>
    /// The interface that loaded types must implement. Stored as a <see cref="Type"/> so that construction is
    /// trim-safe; the actual interface lookup happens by name inside the [RequiresUnreferencedCode] load methods.
    /// </summary>
    private readonly Type _desiredInterface;

    private static readonly string[] runtimeAssemblies = findRuntimeAssembliesWithMicrosoftBuildFramework();

    private static string microsoftBuildFrameworkPath;

    // We need to append Microsoft.Build.Framework from next to the executing assembly first to make sure it's loaded before the runtime variant.
    private static string[] findRuntimeAssembliesWithMicrosoftBuildFramework()
    {
        string msbuildDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        microsoftBuildFrameworkPath = Path.Combine(msbuildDirectory, "Microsoft.Build.Framework.dll");
        string[] msbuildAssemblies = Directory.GetFiles(msbuildDirectory, "*.dll");
        string[] runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll");

        return [.. runtimeAssemblies, .. msbuildAssemblies];
    }

#if NETFRAMEWORK
    private static readonly Lazy<string[]> runtimeAssembliesCLR35_20 = new Lazy<string[]>(FindRuntimeAssembliesWithMicrosoftBuildFrameworkCLR2CLR35);

    /// <summary>
    /// Gathers a list of runtime assemblies for the <see cref="MetadataLoadContext"/>.
    /// This includes assemblies from the MSBuild installation directory, the current .NET runtime directory,
    /// and on .NET Framework, assemblies from older framework versions (2.0, 3.5).
    /// The path to the current `Microsoft.Build.Framework.dll` is also stored to ensure it's prioritized
    /// for resolving essential types like <see cref="ITaskItem"/>.
    /// These paths are used to create a <see cref="PathAssemblyResolver"/> for the <see cref="MetadataLoadContext"/>.
    /// </summary>
    private static string[] FindRuntimeAssembliesWithMicrosoftBuildFrameworkCLR2CLR35()
    {
        string v20Path = FrameworkLocationHelper.PathToDotNetFrameworkV20;
        string v35Path = FrameworkLocationHelper.PathToDotNetFrameworkV35;

        string[] clr2Assemblies = !string.IsNullOrEmpty(v20Path) && Directory.Exists(v20Path)
            ? Directory.GetFiles(v20Path, "*.dll")
            : [];
        string[] clr35Assemblies = !string.IsNullOrEmpty(v35Path) && Directory.Exists(v35Path)
            ? Directory.GetFiles(v35Path, "*.dll")
            : [];

        // Deduplicate: CLR3.5 assemblies take priority over CLR2 assemblies
        Dictionary<string, string> assembliesDictionary = new(StringComparer.OrdinalIgnoreCase);
        AddAssembliesToDictionary(assembliesDictionary, clr2Assemblies, clr35Assemblies);

        return [.. assembliesDictionary.Values];
    }
#endif

    /// <summary>
    /// Constructor.
    /// </summary>
    private TypeLoader(Type desiredInterface)
    {
        _desiredInterface = desiredInterface;
    }

    /// <summary>
    /// Creates a <see cref="TypeLoader"/> that selects concrete, public types implementing <typeparamref name="TInterface"/>
    /// (for example <see cref="ITask"/> or <see cref="ILogger"/>).
    /// </summary>
    /// <remarks>
    /// Capturing the interface as data (rather than a filter delegate) keeps construction trim-safe: no reflection
    /// happens here, so callers in field initializers and other contexts that are not [RequiresUnreferencedCode] do
    /// not produce trim warnings. The interface match runs later, by name, inside the load methods that are already
    /// annotated [RequiresUnreferencedCode]; name matching is also the only test that works for types inspected
    /// through a <see cref="MetadataLoadContext"/>.
    /// </remarks>
    internal static TypeLoader Create<TInterface>() where TInterface : class => new TypeLoader(typeof(TInterface));

    /// <summary>
    /// Delegate used to log warning messages with formatted string support.
    /// </summary>
    /// <param name="format">A composite format string for the warning message.</param>
    /// <param name="args">An array of objects to format into the warning message.</param>
    internal delegate void LogWarningDelegate(string format, params object[] args);

    /// <summary>
    /// Given two type names, looks for a partial match between them. A partial match is considered valid only if it occurs on
    /// the right side (tail end) of the name strings, and at the start of a class or namespace name.
    /// </summary>
    /// <remarks>
    /// 1) Matches are case-insensitive.
    /// 2) .NET conventions regarding namespaces and nested classes are respected, including escaping of reserved characters.
    /// </remarks>
    /// <example>
    /// "Csc" and "csc"                                                 ==> exact match
    /// "Microsoft.Build.Tasks.Csc" and "Microsoft.Build.Tasks.Csc"     ==> exact match
    /// "Microsoft.Build.Tasks.Csc" and "Csc"                           ==> partial match
    /// "Microsoft.Build.Tasks.Csc" and "Tasks.Csc"                     ==> partial match
    /// "MyTasks.ATask+NestedTask" and "NestedTask"                     ==> partial match
    /// "MyTasks.ATask\\+NestedTask" and "NestedTask"                   ==> partial match
    /// "MyTasks.CscTask" and "Csc"                                     ==> no match
    /// "MyTasks.MyCsc" and "Csc"                                       ==> no match
    /// "MyTasks.ATask\.Csc" and "Csc"                                  ==> no match
    /// "MyTasks.ATask\\\.Csc" and "Csc"                                ==> no match
    /// </example>
    /// <returns>true, if the type names match exactly or partially; false, if there is no match at all</returns>
    internal static bool IsPartialTypeNameMatch(string typeName1, string typeName2)
    {
        bool isPartialMatch = false;

        // if the type names are the same length, a partial match is impossible
        if (typeName1.Length != typeName2.Length)
        {
            string longerTypeName;
            string shorterTypeName;

            // figure out which type name is longer
            if (typeName1.Length > typeName2.Length)
            {
                longerTypeName = typeName1;
                shorterTypeName = typeName2;
            }
            else
            {
                longerTypeName = typeName2;
                shorterTypeName = typeName1;
            }

            // if the shorter type name matches the end of the longer one
            if (longerTypeName.EndsWith(shorterTypeName, StringComparison.OrdinalIgnoreCase))
            {
                int matchIndex = longerTypeName.Length - shorterTypeName.Length;

                // if the matched sub-string looks like the start of a namespace or class name
                if ((longerTypeName[matchIndex - 1] == '.') || (longerTypeName[matchIndex - 1] == '+'))
                {
                    int precedingBackslashes = 0;

                    // confirm there are zero, or an even number of \'s preceding it...
                    for (int i = matchIndex - 2; i >= 0; i--)
                    {
                        if (longerTypeName[i] == '\\')
                        {
                            precedingBackslashes++;
                        }
                        else
                        {
                            break;
                        }
                    }

                    if ((precedingBackslashes % 2) == 0)
                    {
                        isPartialMatch = true;
                    }
                }
            }
        }
        else
        {
            isPartialMatch = (String.Equals(typeName1, typeName2, StringComparison.OrdinalIgnoreCase));
        }

        return isPartialMatch;
    }

    /// <summary>
    /// Load an assembly given its AssemblyLoadInfo
    /// </summary>
    /// <param name="assemblyLoadInfo"></param>
    /// <returns></returns>
    [RequiresUnreferencedCode("Loads task and factory assemblies discovered at runtime, which is incompatible with trimming.")]
    private static Assembly LoadAssembly(AssemblyLoadInfo assemblyLoadInfo)
    {
        try
        {
            if (assemblyLoadInfo.AssemblyName != null)
            {
                return Assembly.Load(assemblyLoadInfo.AssemblyName);
            }
            else if (assemblyLoadInfo.IsInlineTask)
            {
                // Load inline task assemblies from bytes and register the path
                return TaskFactoryUtilities.LoadTaskAssembly(assemblyLoadInfo.AssemblyFile);
            }
            else
            {
#if NET && FEATURE_ASSEMBLYLOADCONTEXT
                string baseDir = Path.GetDirectoryName(assemblyLoadInfo.AssemblyFile);
                s_coreClrAssemblyLoader.AddDependencyLocation(baseDir);
                return s_coreClrAssemblyLoader.LoadFromPath(assemblyLoadInfo.AssemblyFile);
#else
                return Assembly.UnsafeLoadFrom(assemblyLoadInfo.AssemblyFile);
#endif
            }
        }
        catch (ArgumentException e)
        {
            // Assembly.Load() and Assembly.LoadFrom() will throw an ArgumentException if the assembly name is invalid
            // convert to a FileNotFoundException because it's more meaningful
            // NOTE: don't use ErrorUtilities.VerifyThrowFileExists() here because that will hit the disk again
            throw new FileNotFoundException(null, assemblyLoadInfo.AssemblyLocation, e);
        }
    }

    private static MetadataLoadContext CreateMetadataLoadContext(AssemblyLoadInfo assemblyLoadInfo)
    {
        string assemblyFilePath = assemblyLoadInfo.AssemblyFile;
        if (string.IsNullOrEmpty(assemblyFilePath) || !File.Exists(assemblyFilePath))
        {
            throw new FileNotFoundException(null, assemblyLoadInfo.AssemblyLocation);
        }

        string assemblyDirectory = Path.GetDirectoryName(assemblyFilePath);
        string[] dlls = Directory.GetFiles(assemblyDirectory, "*.dll");
        string[] exes = Directory.GetFiles(assemblyDirectory, "*.exe");
        string[] localAssemblies = [.. dlls, .. exes];

#if !NETFRAMEWORK

        // Deduplicate between MSBuild assemblies and task dependencies.
        Dictionary<string, string> assembliesDictionary = new(localAssemblies.Length + runtimeAssemblies.Length);
        AddAssembliesToDictionary(assembliesDictionary, localAssemblies, runtimeAssemblies);
#else
        // Merge all assembly tiers into one dictionary with priority:
        // deduplicated CLR2 & CLR3.5 < Local < Runtime (later entries overwrite earlier ones)
        Dictionary<string, string> assembliesDictionary = new(localAssemblies.Length + runtimeAssemblies.Length + runtimeAssembliesCLR35_20.Value.Length);

        // Add assemblies in priority order (later entries overwrite earlier ones)
        AddAssembliesToDictionary(
            assembliesDictionary,
            runtimeAssembliesCLR35_20.Value,
            localAssemblies,
            runtimeAssemblies);
#endif
        return new MetadataLoadContext(new PathAssemblyResolver(assembliesDictionary.Values));
    }

    // MetadataLoadContext can expose a PropertyInfo even when resolving PropertyType fails because a
    // dependency is absent. Reading the PropertySig blob from the declaring assembly avoids resolving that
    // dependency. The result is only an MSBuild expansion surrogate plus a display name for diagnostics; it
    // is not the property's executable Type and does not imply that a particular task host supports it.
    // A default result means the signature could not be classified safely, although individual supported
    // shapes can still return a declared name with no expansion surrogate.
    private static (Type TypeForExpansion, string DeclaredTypeName) ReadParameterTypeInfo(PropertyInfo propertyInfo)
    {
        try
        {
#if NET
            string assemblyPath = RuntimeFeature.IsDynamicCodeSupported
                ? propertyInfo.DeclaringType?.Assembly.Location ?? string.Empty
                : string.Empty;
#else
            string assemblyPath = propertyInfo.DeclaringType?.Assembly.Location ?? string.Empty;
#endif
            using FileStream stream = File.OpenRead(assemblyPath);
            using var peReader = new PEReader(stream);
            MetadataReader metadataReader = peReader.GetMetadataReader();
            PropertyDefinition property = metadataReader.GetPropertyDefinition(
                (PropertyDefinitionHandle)MetadataTokens.EntityHandle(propertyInfo.MetadataToken));
            BlobReader signature = metadataReader.GetBlobReader(property.Signature);
            return ReadParameterTypeInfo(ref signature, metadataReader);
        }
        catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
        {
            return default;
        }
    }

    internal static Type ReadParameterTypeForExpansion(ref BlobReader signature, MetadataReader metadataReader) =>
        ReadParameterTypeInfo(ref signature, metadataReader).TypeForExpansion;

    private static (Type TypeForExpansion, string DeclaredTypeName) ReadParameterTypeInfo(
        ref BlobReader signature,
        MetadataReader metadataReader)
    {
        // ECMA-335 II.23.2.5 defines PropertySig as its calling-convention header, a compressed
        // parameter count, the property type, and any index parameters. MSBuild task parameters are
        // non-generic properties without index parameters; their return type follows the Type grammar
        // in II.23.2.12.
        SignatureHeader header = signature.ReadSignatureHeader();
        if (header.Kind != SignatureKind.Property || header.IsGeneric || signature.ReadCompressedInteger() != 0)
        {
            return default;
        }

        (Type TypeForExpansion, string DeclaredTypeName) parameterType =
            ReadParameterTypeInfo(ref signature, metadataReader, depth: 0);
        // A successful prefix is not enough: trailing bytes would mean an unsupported or malformed shape.
        return signature.RemainingBytes == 0 ? parameterType : default;
    }

    private static (Type TypeForExpansion, string DeclaredTypeName) ReadParameterTypeInfo(
        ref BlobReader signature,
        MetadataReader metadataReader,
        int depth)
    {
        // Bound Type nesting independently from the declaring-type walk below.
        if (depth >= 16)
        {
            return default;
        }

        // This is the Type grammar's element-type tag. Class and value-type tags are followed by a
        // separately encoded TypeDefOrRef handle, decoded below with ReadTypeHandle.
        int typeCode = signature.ReadCompressedInteger();
        if (typeCode == (int)SignatureTypeCode.SZArray)
        {
            // Only one-dimensional, zero-based arrays are expansion surrogates. Nested arrays and the
            // multidimensional ARRAY form remain unsupported. Text-converted elements use string[],
            // while typed task items retain ITaskItem[] so item identity and metadata survive transport.
            (Type TypeForExpansion, string DeclaredTypeName) elementType =
                ReadParameterTypeInfo(ref signature, metadataReader, depth + 1);
            return (
                elementType.TypeForExpansion == typeof(ITaskItem)
                    ? typeof(ITaskItem[])
                    : elementType.TypeForExpansion?.IsArray == false
                        ? LoadedType.GetArrayExpansionType(elementType.TypeForExpansion)
                    : null,
                elementType.DeclaredTypeName is null ? null : $"{elementType.DeclaredTypeName}[]");
        }

        if (typeCode is >= (int)SignatureTypeCode.Boolean and <= (int)SignatureTypeCode.String)
        {
            // MSBuild expands intrinsic scalar values from text; the normal binder or task host performs
            // the final conversion to the declared primitive type.
            return (typeof(string), $"System.{(SignatureTypeCode)typeCode}");
        }

        if (metadataReader is not null && typeCode == (int)SignatureTypeCode.GenericTypeInstance)
        {
            // Per II.23.2.12, GENERICINST is followed by CLASS/VALUETYPE, a TypeDefOrRefEncoded generic
            // definition, a compressed argument count, and the argument Types. Only the Framework's
            // supported ITaskItem<T>/TaskItem<T> forms can use ordinary ITaskItem transport. Other generic
            // forms are rejected; a supported Framework definition with an unsupported argument can still
            // retain its declared name for the MSB4069 diagnostic.
            int genericTypeKind = signature.ReadCompressedInteger();
            if (genericTypeKind is not ((int)SignatureTypeKind.Class) and not ((int)SignatureTypeKind.ValueType))
            {
                return default;
            }

            EntityHandle genericTypeHandle = signature.ReadTypeHandle();
            string genericTypeName = genericTypeHandle.Kind is HandleKind.TypeDefinition or HandleKind.TypeReference
                ? GetFullName(genericTypeHandle, depth: 0)
                : null;
            if (!IsFrameworkTaskItemType(genericTypeHandle, genericTypeName)
                || signature.ReadCompressedInteger() != 1)
            {
                return default;
            }

            (Type _, string DeclaredTypeName) argumentType =
                ReadParameterTypeInfo(ref signature, metadataReader, depth + 1);
            string declaredTypeName = argumentType.DeclaredTypeName is null
                ? null
                : $"{genericTypeName}<{argumentType.DeclaredTypeName}>";
            if (!IsSupportedTaskItemValueType(argumentType.DeclaredTypeName))
            {
                return (null, declaredTypeName);
            }

            return (typeof(ITaskItem), declaredTypeName);
        }

        if (metadataReader is null
            || typeCode is not ((int)SignatureTypeKind.Class) and not ((int)SignatureTypeKind.ValueType))
        {
            return default;
        }

        // CLASS and VALUETYPE are followed by a TypeDefOrRefEncoded handle. Named path types keep their
        // path-aware expansion type; other value types expand from strings. Unsupported reference types
        // have no surrogate, but their recoverable declared name is still useful diagnostically.
        EntityHandle handle = signature.ReadTypeHandle();
        if (handle.Kind is not HandleKind.TypeDefinition and not HandleKind.TypeReference)
        {
            return default;
        }

        string fullName = GetFullName(handle, depth: 0);
        return (fullName switch
        {
            string name when name == typeof(AbsolutePath).FullName => typeof(AbsolutePath),
            string name when name == typeof(FileInfo).FullName => typeof(FileInfo),
            string name when name == typeof(DirectoryInfo).FullName => typeof(DirectoryInfo),
            _ when typeCode == (int)SignatureTypeKind.ValueType => typeof(string),
            _ => null,
        }, fullName);

        string GetFullName(EntityHandle typeHandle, int depth)
        {
            // Bound declaring-type traversal separately from Type nesting. Metadata represents nested
            // declarations through their enclosing type; diagnostics use reflection's '+' spelling and
            // omit a namespace prefix for global-namespace types.
            if (depth >= 16)
            {
                return null;
            }

            StringHandle namespaceHandle;
            StringHandle nameHandle;
            EntityHandle declaringType = default;
            if (typeHandle.Kind == HandleKind.TypeDefinition)
            {
                TypeDefinition type = metadataReader.GetTypeDefinition((TypeDefinitionHandle)typeHandle);
                namespaceHandle = type.Namespace;
                nameHandle = type.Name;
                declaringType = type.GetDeclaringType();
            }
            else
            {
                TypeReference type = metadataReader.GetTypeReference((TypeReferenceHandle)typeHandle);
                namespaceHandle = type.Namespace;
                nameHandle = type.Name;
                if (type.ResolutionScope.Kind == HandleKind.TypeReference)
                {
                    declaringType = (TypeReferenceHandle)type.ResolutionScope;
                }
            }

            string name = metadataReader.GetString(nameHandle);
            if (!declaringType.IsNil)
            {
                string declaringTypeName = GetFullName(declaringType, depth + 1);
                return declaringTypeName is null ? null : $"{declaringTypeName}+{name}";
            }

            string @namespace = metadataReader.GetString(namespaceHandle);
            return @namespace.Length == 0 ? name : $"{@namespace}.{name}";
        }

        bool IsFrameworkTaskItemType(EntityHandle typeHandle, string fullName)
        {
            if (typeHandle.Kind != HandleKind.TypeReference
                || (fullName != typeof(ITaskItem<>).FullName && fullName != typeof(TaskItem<>).FullName))
            {
                return false;
            }

            TypeReference typeReference = metadataReader.GetTypeReference((TypeReferenceHandle)typeHandle);
            if (typeReference.ResolutionScope.Kind != HandleKind.AssemblyReference)
            {
                return false;
            }

            AssemblyReference assemblyReference =
                metadataReader.GetAssemblyReference((AssemblyReferenceHandle)typeReference.ResolutionScope);
            AssemblyName frameworkAssembly = typeof(ITaskItem).Assembly.GetName();
            if (!metadataReader.StringComparer.Equals(assemblyReference.Name, frameworkAssembly.Name))
            {
                return false;
            }

            byte[] expectedToken = frameworkAssembly.GetPublicKeyToken();
            BlobReader token = metadataReader.GetBlobReader(assemblyReference.PublicKeyOrToken);
            return expectedToken is not null
                && token.Length == expectedToken.Length
                && token.ReadBytes(token.Length).SequenceEqual(expectedToken);
        }

        static bool IsSupportedTaskItemValueType(string fullName)
        {
            if (fullName is null)
            {
                return false;
            }

            Type type = TaskParameterTypeRegistry.TryGetType(fullName)
                ?? (fullName == typeof(AbsolutePath).FullName ? typeof(AbsolutePath) :
                    fullName == typeof(FileInfo).FullName ? typeof(FileInfo) :
                    fullName == typeof(DirectoryInfo).FullName ? typeof(DirectoryInfo) :
                    null);
            return type is not null && ValueTypeParser.IsSupportedType(type);
        }
    }

    /// <summary>
    /// Adds assembly paths to a dictionary, keyed by file name.
    /// Later arrays in the parameter list take priority over earlier ones,
    /// as duplicate file names will overwrite existing entries.
    /// </summary>
    /// <param name="assembliesDictionary">The dictionary to populate with assembly paths, keyed by file name.</param>
    /// <param name="assemblyPathArrays">Arrays of assembly file paths to add, in order of increasing priority.</param>
    private static void AddAssembliesToDictionary(Dictionary<string, string> assembliesDictionary, params string[][] assemblyPathArrays)
    {
        foreach (string[] assemblyPaths in assemblyPathArrays)
        {
            foreach (string path in assemblyPaths)
            {
                assembliesDictionary[Path.GetFileName(path)] = path;
            }
        }
    }

    /// <summary>
    /// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if
    /// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type
    /// found will be returned.
    /// </summary>
    [RequiresUnreferencedCode("Loads types by reflecting over assemblies discovered at runtime, which is incompatible with trimming.")]
    internal LoadedType Load(
        string typeName,
        AssemblyLoadInfo assembly,
        LogWarningDelegate logWarning,
        bool useTaskHost = false,
        bool taskHostParamsMatchCurrentProc = true)
    {
        return GetLoadedType(s_cacheOfLoadedTypesByFilter, typeName, assembly, useTaskHost, taskHostParamsMatchCurrentProc, logWarning);
    }

    /// <summary>
    /// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if
    /// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type
    /// found will be returned.
    /// </summary>
    /// <returns>The loaded type, or null if the type was not found.</returns>
    [RequiresUnreferencedCode("Loads types by reflecting over assemblies discovered at runtime, which is incompatible with trimming.")]
    internal LoadedType ReflectionOnlyLoad(
        string typeName,
        AssemblyLoadInfo assembly) => GetLoadedType(s_cacheOfReflectionOnlyLoadedTypesByFilter, typeName, assembly, useTaskHost: false, taskHostParamsMatchCurrentProc: true, logWarning: (format, args) => { });

    /// <summary>
    /// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if
    /// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type
    /// found will be returned.
    /// </summary>
    [RequiresUnreferencedCode("Loads types by reflecting over assemblies discovered at runtime, which is incompatible with trimming.")]
    private LoadedType GetLoadedType(
        ConcurrentDictionary<Type, ConcurrentDictionary<AssemblyLoadInfo, AssemblyInfoToLoadedTypes>> cache,
        string typeName,
        AssemblyLoadInfo assembly,
        bool useTaskHost,
        bool taskHostParamsMatchCurrentProc,
        LogWarningDelegate logWarning)
    {
        // A given interface has been used on a number of assemblies. Based on the interface we will get another dictionary which
        // will map a specific AssemblyLoadInfo to a AssemblyInfoToLoadedTypes class which knows how to find a typeName in a given assembly.
        ConcurrentDictionary<AssemblyLoadInfo, AssemblyInfoToLoadedTypes> loadInfoToType =
            cache.GetOrAdd(_desiredInterface, (_) => new ConcurrentDictionary<AssemblyLoadInfo, AssemblyInfoToLoadedTypes>());

        // Get an object which is able to take a typename and determine if it is in the assembly pointed to by the AssemblyInfo.
        AssemblyInfoToLoadedTypes typeNameToType =
            loadInfoToType.GetOrAdd(assembly, (_) => new AssemblyInfoToLoadedTypes(_desiredInterface, _));

        return typeNameToType.GetLoadedTypeByTypeName(typeName, useTaskHost, taskHostParamsMatchCurrentProc, logWarning);
    }

    /// <summary>
    /// Given a type filter and an asssemblyInfo object keep track of what types in a given assembly which match the type filter.
    /// Also, use this information to determine if a given TypeName is in the assembly which is pointed to by the AssemblyLoadInfo object.
    ///
    /// This type represents a combination of a type filter and an assemblyInfo object.
    /// </summary>
    [DebuggerDisplay("Types in {_assemblyLoadInfo} matching {_desiredInterface}")]
    private class AssemblyInfoToLoadedTypes
    {
        /// <summary>
        /// Lock to prevent two threads from using this object at the same time.
        /// Since we fill up internal structures with what is in the assembly
        /// </summary>
        private readonly LockType _lockObject = new();

        /// <summary>
        /// The interface that selected types must implement.
        /// </summary>
        private readonly Type _desiredInterface;

        /// <summary>
        /// The full name of <see cref="_desiredInterface"/>, cached for the by-name interface lookup.
        /// </summary>
        private readonly string _desiredInterfaceName;

        /// <summary>
        /// Assembly load information so we can load an assembly
        /// </summary>
        private AssemblyLoadInfo _assemblyLoadInfo;

        /// <summary>
        /// What is the type for the given type name, this may be null if the typeName does not map to a type.
        /// </summary>
        private ConcurrentDictionary<string, Type> _typeNameToType;

        /// <summary>
        /// List of public types in the assembly which match the type filter and their corresponding types
        /// </summary>
        private Dictionary<string, Type> _publicTypeNameToType;

        private ConcurrentDictionary<string, LoadedType> _publicTypeNameToLoadedType;

        /// <summary>
        /// Have we scanned the public types for this assembly yet.
        /// </summary>
        private long _haveScannedPublicTypes;

        /// <summary>
        /// Assembly, if any, that we loaded for this type.
        /// We use this information to set the LoadedType.LoadedAssembly so that this object can be used
        /// to help created AppDomains to resolve those that it could not load successfully
        /// </summary>
        private Assembly _loadedAssembly;

        /// <summary>
        /// The architecture requirement of the assembly.
        /// </summary>
        private string _architecture;

        /// <summary>
        /// The runtime requirement of the assembly.
        /// Detected by examining referenced assemblies for System.Runtime (indicates .NET Core/5+).
        /// </summary>
        private string _runtime;

        /// <summary>
        /// Flag to track if we've already attempted to get assembly runtime/architecture.
        /// This prevents repeated expensive PE header reads.
        /// </summary>
        private volatile bool _hasReadRuntimeAndArchitecture;

        /// <summary>
        /// Given a desired interface, and an assembly to load the type information from determine if a given type name is in the assembly or not.
        /// </summary>
        internal AssemblyInfoToLoadedTypes(Type desiredInterface, AssemblyLoadInfo loadInfo)
        {
            ArgumentNullException.ThrowIfNull(desiredInterface);
            ArgumentNullException.ThrowIfNull(loadInfo);

            _desiredInterface = desiredInterface;
            _desiredInterfaceName = desiredInterface.FullName;
            _assemblyLoadInfo = loadInfo;
            _typeNameToType = new(StringComparer.OrdinalIgnoreCase);
            _publicTypeNameToType = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
            _publicTypeNameToLoadedType = new(StringComparer.OrdinalIgnoreCase);
        }

        /// <summary>
        /// Determines whether <paramref name="type"/> is a concrete, public class that implements the desired interface.
        /// </summary>
        /// <remarks>
        /// The interface is matched by name rather than with <c>typeof(...).IsAssignableFrom</c> because the candidate
        /// type may have been inspected through a <see cref="MetadataLoadContext"/>, whose reflection universe is
        /// separate from the running one (so <c>IsAssignableFrom</c> would always be false). The
        /// <see cref="Type.GetInterface(string)"/> call is trim-unsafe, but this method is only reachable from the
        /// [RequiresUnreferencedCode] load paths, which load task and logger assemblies discovered at runtime.
        /// </remarks>
        [RequiresUnreferencedCode("Matches a runtime-discovered type against the desired interface by reflecting over its interface list, which is incompatible with trimming.")]
        private bool IsDesiredType(Type type) =>
            type.IsClass && !type.IsAbstract && type.GetInterface(_desiredInterfaceName) is not null;

        /// <summary>
        /// Determine if a given type name is in the assembly or not. Return null if the type is not in the assembly.
        /// </summary>
        [RequiresUnreferencedCode("Loads types by reflecting over assemblies discovered at runtime, which is incompatible with trimming.")]
        internal LoadedType GetLoadedTypeByTypeName(
            string typeName,
            bool useTaskHost,
            bool taskHostParamsMatchCurrentProc,
            LogWarningDelegate logWarning)
        {
            ArgumentNullException.ThrowIfNull(typeName);

            if (ShouldUseMetadataLoadContext(useTaskHost, taskHostParamsMatchCurrentProc))
            {
                return GetTypeForOutOfProcExecution(typeName);
            }

            LoadedType loadedType;

            try
            {
                loadedType = LoadInProc(typeName);
            }
            catch
            {
                // The assembly can't be loaded in-proc due to architecture or runtime mismatch that was discovered during in-proc load.
                // Fall back to metadata load context. It will prepare prerequisites for out of proc execution.
                MSBuildEventSource.Log.FallbackAssemblyLoadStart(typeName);
                loadedType = GetTypeForOutOfProcExecution(typeName);
                logWarning("AssemblyLoad_Warning", loadedType?.LoadedAssemblyName?.Name);
                MSBuildEventSource.Log.FallbackAssemblyLoadStop(typeName);
            }

            return loadedType;
        }

        /// <summary>
        /// Normal in-proc loading path.
        /// Only one thread should be doing operations on this instance of the object at a time
        /// This loads the assembly for actual execution (not metadata-only).
        /// </summary>
        /// <param name="typeName">The type to be loaded.</param>
        [RequiresUnreferencedCode("Loads types by reflecting over assemblies discovered at runtime, which is incompatible with trimming.")]
        private LoadedType LoadInProc(string typeName)
        {
            Type type = _typeNameToType.GetOrAdd(typeName, (key) =>
            {
                if ((_assemblyLoadInfo.AssemblyName != null) && (typeName.Length > 0))
                {
                    try
                    {
                        // try to load the type using its assembly qualified name
                        Type t2 = Type.GetType(typeName + "," + _assemblyLoadInfo.AssemblyName, false /* don't throw on error */, true /* case-insensitive */);
                        if (t2 != null)
                        {
                            return !IsDesiredType(t2) ? null : t2;
                        }
                    }
                    catch (ArgumentException)
                    {
                        // Type.GetType() will throw this exception if the type name is invalid -- but we have no idea if it's the
                        // type or the assembly name that's the problem -- so just ignore the exception, because we're going to
                        // check the existence/validity of the assembly and type respectively, below anyway
                    }
                }

                if (Interlocked.Read(ref _haveScannedPublicTypes) == 0)
                {
                    lock (_lockObject)
                    {
                        if (Interlocked.Read(ref _haveScannedPublicTypes) == 0)
                        {
                            ScanAssemblyForPublicTypes();
                            Interlocked.Exchange(ref _haveScannedPublicTypes, ~0);
                        }
                    }
                }

                foreach (KeyValuePair<string, Type> desiredTypeInAssembly in _publicTypeNameToType)
                {
                    // if type matches partially on its name
                    if (typeName.Length == 0 || IsPartialTypeNameMatch(desiredTypeInAssembly.Key, typeName))
                    {
                        return desiredTypeInAssembly.Value;
                    }
                }

                return null;
            });

            return type != null
                ? LoadedType.CreateWithParameterTypeInfoResolver(
                    type,
                    _assemblyLoadInfo,
                    _loadedAssembly ?? type.Assembly,
                    typeof(ITaskItem),
                    runtime: null,
                    architecture: null,
                    loadedViaMetadataLoadContext: false,
                    parameterTypeInfoResolver: ReadParameterTypeInfo)
                : null;
        }

        /// <summary>
        /// Determine whether an assembly is likely to be used out of process and thus loaded with a <see cref="MetadataLoadContext"/>.
        /// </summary>
        /// <param name="useTaskHost">Task Host Parameter was specified explicitly in XML or through environment variable.</param>
        /// <param name="taskHostParamsMatchCurrentProc">The parameter defines if Runtime/Architecture explicitly defined in XML match current process.</param>
        private bool ShouldUseMetadataLoadContext(bool useTaskHost, bool taskHostParamsMatchCurrentProc) =>
            (useTaskHost || !taskHostParamsMatchCurrentProc) && _assemblyLoadInfo.AssemblyFile is not null;

        [RequiresUnreferencedCode("Loads types by reflecting over assemblies discovered at runtime, which is incompatible with trimming.")]
        private LoadedType GetTypeForOutOfProcExecution(string typeName) => _publicTypeNameToLoadedType
            .GetOrAdd(typeName, typeName =>
            {
                MSBuildEventSource.Log.LoadAssemblyAndFindTypeStart();
                using MetadataLoadContext context = CreateMetadataLoadContext(_assemblyLoadInfo);
                Assembly loadedAssembly = context.LoadFromAssemblyPath(_assemblyLoadInfo.AssemblyFile);
                SetArchitectureAndRuntime(loadedAssembly);

                Type foundType = null;
                int numberOfTypesSearched = 0;

                // Try direct type lookup first (fastest)
                if (!string.IsNullOrEmpty(typeName))
                {
                    foundType = loadedAssembly.GetType(typeName, throwOnError: false);
                    if (foundType != null && foundType.IsPublic && IsDesiredType(foundType))
                    {
                        numberOfTypesSearched = 1;
                    }
                }

                // Fallback: enumerate all types for partial matching
                if (foundType == null)
                {
                    foreach (Type publicType in loadedAssembly.GetExportedTypes())
                    {
                        numberOfTypesSearched++;
                        try
                        {
                            if (IsDesiredType(publicType) && (typeName.Length == 0 || IsPartialTypeNameMatch(publicType.FullName, typeName)))
                            {
                                foundType = publicType;
                                break;
                            }
                        }
                        catch
                        {
                            // Ignore types that can't be loaded/reflected upon.
                            // These types might be needed out of proc and be resolved there.
                        }
                    }
                }

                if (foundType != null)
                {
                    MSBuildEventSource.Log.CreateLoadedTypeStart(loadedAssembly.FullName);
                    var taskItemType = context.LoadFromAssemblyPath(microsoftBuildFrameworkPath).GetType(typeof(ITaskItem).FullName);
                    LoadedType loadedType = LoadedType.CreateWithParameterTypeInfoResolver(
                        foundType,
                        _assemblyLoadInfo,
                        loadedAssembly,
                        taskItemType,
                        _runtime,
                        _architecture,
                        loadedViaMetadataLoadContext: true,
                        parameterTypeInfoResolver: ReadParameterTypeInfo);

                    MSBuildEventSource.Log.CreateLoadedTypeStop(loadedAssembly.FullName);
                    return loadedType;
                }

                MSBuildEventSource.Log.LoadAssemblyAndFindTypeStop(_assemblyLoadInfo.AssemblyFile, numberOfTypesSearched);

                return null;
            });

        /// <summary>
        /// Gets architecture and runtime from the assembly using MetadataLoadContext.
        /// </summary>
        [RequiresUnreferencedCode("Reflects over a runtime-loaded assembly to determine its target runtime and architecture, which is incompatible with trimming.")]
        private void SetArchitectureAndRuntime(Assembly assembly)
        {
            if (_hasReadRuntimeAndArchitecture)
            {
                return;
            }

            try
            {
                SetRuntime();
                SetArchitecture();
                _hasReadRuntimeAndArchitecture = true;
            }
            catch
            {
                // If we fail to read the assembly for any reason don't throw, just reset the values.
                _architecture = null;
                _runtime = null;
                _hasReadRuntimeAndArchitecture = false;
            }

            void SetRuntime()
            {
                string targetFramework = null;
                try
                {
                    CustomAttributeData targetFrameworkAttr = assembly?
                        .GetCustomAttributesData()?
                        .FirstOrDefault(a => a.AttributeType.Name == TargetFrameworkAttributeName && a.AttributeType.Namespace == VersioningNamespaceName);

                    if (targetFrameworkAttr != null && targetFrameworkAttr.ConstructorArguments.Count > 0)
                    {
                        // the final value looks like: ".NETFramework,Version=v3.5"
                        targetFramework = targetFrameworkAttr.ConstructorArguments[0].Value as string ?? string.Empty;
                        _runtime = targetFramework.StartsWith(DotNetCoreIdentifier, StringComparison.Ordinal) ? MSBuildRuntimeValues.net : MSBuildRuntimeValues.clr4;
                    }
                }
                catch
                {
                    // something went wrong with reading the custom attribute!
                }

                if (targetFramework == null && _runtime == null)
                {
                    bool hasSystemRuntime = assembly.GetReferencedAssemblies().Any(a => string.Equals(a.Name, SystemRuntimeAssemblyName, StringComparison.OrdinalIgnoreCase));
                    if (hasSystemRuntime)
                    {
                        _runtime = MSBuildRuntimeValues.net;
                    }
                }
            }

            void SetArchitecture()
            {
                Module module = assembly?.Modules?.FirstOrDefault();
                if (module == null)
                {
                    return;
                }

                module.GetPEKind(out PortableExecutableKinds peKind, out ImageFileMachine machine);

                bool isILOnly = (peKind & PortableExecutableKinds.ILOnly) != 0;
                bool requires32Bit = (peKind & PortableExecutableKinds.Required32Bit) != 0;
                bool prefers32Bit = (peKind & PortableExecutableKinds.Preferred32Bit) != 0;

                if (requires32Bit || prefers32Bit)
                {
                    _architecture = MSBuildArchitectureValues.x86;
                    return;
                }

                if (isILOnly && machine == ImageFileMachine.I386)
                {
                    _architecture = MSBuildArchitectureValues.any;
                    return;
                }

                _architecture = machine switch
                {
                    ImageFileMachine.I386 => MSBuildArchitectureValues.x86,
                    ImageFileMachine.AMD64 => MSBuildArchitectureValues.x64,
                    (ImageFileMachine)0xAA64 => MSBuildArchitectureValues.arm64,
                    _ => MSBuildArchitectureValues.any,
                };
            }
        }

        /// <summary>
        /// Scan the assembly pointed to by the assemblyLoadInfo for public types. We will use these public types to do partial name matching on
        /// to find tasks, loggers, and task factories.
        /// </summary>
        [RequiresUnreferencedCode("Loads and reflects over a runtime-discovered assembly's public types, which is incompatible with trimming.")]
        private void ScanAssemblyForPublicTypes()
        {
            // we need to search the assembly for the type...
            _loadedAssembly = LoadAssembly(_assemblyLoadInfo);

            // only look at public types
            Type[] allPublicTypesInAssembly = _loadedAssembly.GetExportedTypes();
            foreach (Type publicType in allPublicTypesInAssembly)
            {
                if (IsDesiredType(publicType))
                {
                    _publicTypeNameToType.Add(publicType.FullName, publicType);
                }
            }
        }
    }
}