File: TargetFrameworkUtil.cs
Web Access
Project: src\src\Compilers\Test\Core\Microsoft.CodeAnalysis.Test.Utilities.csproj (Microsoft.CodeAnalysis.Test.Utilities)
// 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;
using System.Linq;
using System.IO;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Test.Utilities;
using Basic.Reference.Assemblies;
using Microsoft.CodeAnalysis.CodeGen;
using System.Reflection;
using System.Collections.Concurrent;
using static TestReferences;
 
namespace Roslyn.Test.Utilities
{
    public enum TargetFramework
    {
        /// <summary>
        /// Explicit pick a target framework that has no references
        /// </summary>
        Empty,
 
        NetStandard20,
 
        /// <summary>
        /// The latest .NET Core target framework
        /// </summary>
        NetCoreApp,
 
        /// <summary>
        /// The latest .NET Framework
        /// </summary>
        NetFramework,
 
        /// <summary>
        /// This will be <see cref="NetCoreApp" /> when running on .NET Core and <see cref="NetFramework"/>
        /// when running on .NET Framework.
        /// </summary>
        NetLatest,
 
        // Eventually these will be deleted and replaced with NetStandard20. Short term this creates the "standard"
        // API set across desktop and coreclr. It's also helpful because there are no null annotations hence error
        // messages have consistent signatures across .NET Core / Framework tests.
        Standard,
        StandardAndCSharp,
        StandardAndVBRuntime,
 
        /// <summary>
        /// Compat framework for the default set of references many vb compilations get.
        /// </summary>
        DefaultVb,
 
        /// <summary>
        /// Used for building tests against WinRT scenarios
        /// </summary>
        WinRT,
 
        // The flavors of mscorlib we support + extending them with LINQ and dynamic.
        Mscorlib40,
        Mscorlib40Extended,
        Mscorlib40AndSystemCore,
        Mscorlib40AndVBRuntime,
        Mscorlib46,
        Mscorlib46Extended,
        Mscorlib461,
        Mscorlib461Extended,
        Mscorlib461AndCSharp,
        Mscorlib461AndVBRuntime,
 
        /// <summary>
        /// Minimal set of required types (<see cref="NetFx.Minimal.mincorlib"/>).
        /// </summary>
        Minimal,
 
        /// <summary>
        /// Minimal set of required types and Task implementation (<see cref="NetFx.Minimal.minasync"/>).
        /// </summary>
        MinimalAsync,
 
        Net50,
        Net60,
        Net70,
        Net80,
        Net90,
    }
 
    /// <summary>
    /// This type holds the reference information for the latest .NET Core platform. Tests
    /// targeting .NET core specifically should use the references here. As the platform moves
    /// forward these will be moved to target the latest .NET Core supported by the compiler
    /// </summary>
    public static class NetCoreApp
    {
        public static ImmutableArray<Net70.ReferenceInfo> AllReferenceInfos { get; } = ImmutableArray.CreateRange(Net70.ReferenceInfos.All);
        public static ImmutableArray<MetadataReference> References { get; } = ImmutableArray.CreateRange<MetadataReference>(Net70.References.All);
 
        public static PortableExecutableReference netstandard { get; } = Net70.References.netstandard;
        public static PortableExecutableReference mscorlib { get; } = Net70.References.mscorlib;
        public static PortableExecutableReference SystemRuntime { get; } = Net70.References.SystemRuntime;
    }
 
    /// <summary>
    /// This type holds the reference information for the latest .NET Framework. These should be
    /// used by tests that are specific to .NET Framework. This moves forward much more rarely but
    /// when it does move forward these will change
    /// </summary>
    public static class NetFramework
    {
        /// <summary>
        /// This is the full set of references provided by default on the .NET Framework TFM
        /// </summary>
        /// <remarks>
        /// Need to special case tuples until we move to net472
        /// </remarks>
        public static ImmutableArray<MetadataReference> References { get; } = [.. Net461.References.All, Net461.ExtraReferences.SystemValueTuple];
 
        /// <summary>
        /// This is a limited set of references on this .NET Framework TFM. This should be avoided in new code 
        /// as it represents the way reference hookup used to work.
        /// </summary>
        /// <remarks>
        /// Need to special case tuples until we move to net472
        /// </remarks>
        public static ImmutableArray<MetadataReference> Standard { get; } =
            [
                Net461.References.mscorlib,
                Net461.References.System,
                Net461.References.SystemCore,
                Net461.References.SystemData,
                Net461.References.SystemRuntime,
                Net461.ExtraReferences.SystemValueTuple
            ];
 
        public static PortableExecutableReference mscorlib { get; } = Net461.References.mscorlib;
        public static PortableExecutableReference System { get; } = Net461.References.System;
        public static PortableExecutableReference SystemRuntime { get; } = Net461.References.SystemRuntime;
        public static PortableExecutableReference SystemCore { get; } = Net461.References.SystemCore;
        public static PortableExecutableReference SystemData { get; } = Net461.References.SystemData;
        public static PortableExecutableReference SystemThreadingTasks { get; } = Net461.References.SystemThreadingTasks;
        public static PortableExecutableReference SystemXml { get; } = Net461.References.SystemXml;
        public static PortableExecutableReference SystemValueTuple { get; } = Net461.ExtraReferences.SystemValueTuple;
        public static PortableExecutableReference MicrosoftCSharp { get; } = Net461.References.MicrosoftCSharp;
        public static PortableExecutableReference MicrosoftVisualBasic { get; } = Net461.References.MicrosoftVisualBasic;
    }
 
    public static class TargetFrameworkUtil
    {
        private static readonly ConcurrentDictionary<string, ImmutableArray<PortableExecutableReference>> s_dynamicReferenceMap = new ConcurrentDictionary<string, ImmutableArray<PortableExecutableReference>>(StringComparer.Ordinal);
 
        public static ImmutableArray<MetadataReference> NetLatest => RuntimeUtilities.IsCoreClrRuntime ? NetCoreApp.References : NetFramework.References;
        public static ImmutableArray<MetadataReference> StandardReferences => RuntimeUtilities.IsCoreClrRuntime ? NetStandard20References : NetFramework.Standard;
        public static MetadataReference StandardCSharpReference => RuntimeUtilities.IsCoreClrRuntime ? NetStandard20.ExtraReferences.MicrosoftCSharp : NetFramework.MicrosoftCSharp;
        public static MetadataReference StandardVisualBasicReference => RuntimeUtilities.IsCoreClrRuntime ? NetStandard20.ExtraReferences.MicrosoftVisualBasic : NetFramework.MicrosoftVisualBasic;
        public static ImmutableArray<MetadataReference> StandardAndCSharpReferences => StandardReferences.Add(StandardCSharpReference);
        public static ImmutableArray<MetadataReference> StandardAndVBRuntimeReferences => StandardReferences.Add(StandardVisualBasicReference);
 
        /*
         * ⚠ Dev note ⚠: properties in TestBase are backed by Lazy<T>. Avoid changes to the following properties
         * which would force the initialization of these properties in the static constructor, since the stack traces
         * for a TypeLoadException are missing important information for resolving problems if/when they occur.
         * https://github.com/dotnet/roslyn/issues/25961
         */
        public static ImmutableArray<MetadataReference> WinRTReferences =>
        [
            .. TestBase.WinRtRefs
        ];
        public static ImmutableArray<MetadataReference> MinimalReferences =>
        [
            TestBase.MinCorlibRef
        ];
        public static ImmutableArray<MetadataReference> MinimalAsyncReferences =>
        [
            TestBase.MinAsyncCorlibRef
        ];
 
        /*
         * ⚠ Dev note ⚠: TestBase properties end here.
         */
 
        public static ImmutableArray<MetadataReference> Mscorlib45ExtendedReferences { get; } =
        [
            NetFramework.mscorlib,
            NetFramework.System,
            NetFramework.SystemCore,
            NetFramework.SystemRuntime,
            NetFramework.SystemValueTuple,
        ];
        public static ImmutableArray<MetadataReference> Mscorlib46ExtendedReferences { get; } =
        [
            Net461.References.mscorlib,
            Net461.References.System,
            Net461.References.SystemCore,
            Net461.References.SystemRuntime,
            Net461.ExtraReferences.SystemValueTuple,
        ];
        public static ImmutableArray<MetadataReference> Mscorlib40References { get; } =
        [
            Net40.References.mscorlib
        ];
        public static ImmutableArray<MetadataReference> Mscorlib40ExtendedReferences { get; } =
        [
            Net40.References.mscorlib,
            Net40.References.System,
            Net40.References.SystemCore
        ];
        public static ImmutableArray<MetadataReference> Mscorlib40andSystemCoreReferences { get; } =
        [
            Net40.References.mscorlib,
            Net40.References.SystemCore
        ];
        public static ImmutableArray<MetadataReference> Mscorlib40andVBRuntimeReferences { get; } =
        [
            Net40.References.mscorlib,
            Net40.References.System,
            Net40.References.MicrosoftVisualBasic
        ];
        public static ImmutableArray<MetadataReference> Mscorlib45References { get; } =
        [
            NetFramework.mscorlib
        ];
        public static ImmutableArray<MetadataReference> Mscorlib46References { get; } =
        [
            Net461.References.mscorlib
        ];
        public static ImmutableArray<MetadataReference> Mscorlib461References { get; } =
        [
            Net461.References.mscorlib
        ];
        public static ImmutableArray<MetadataReference> Mscorlib461ExtendedReferences { get; } =
        [
            Net461.References.mscorlib,
            Net461.References.System,
            Net461.References.SystemCore,
            Net461.References.SystemRuntime,
            Net461.ExtraReferences.SystemValueTuple,
        ];
        public static ImmutableArray<MetadataReference> Mscorlib461AndCSharpReferences { get; } =
        [
            Net461.References.mscorlib,
            Net461.References.SystemCore,
            Net461.References.MicrosoftCSharp
        ];
        public static ImmutableArray<MetadataReference> Mscorlib461AndVBRuntimeReferences { get; } =
        [
            Net461.References.mscorlib,
            Net461.References.System,
            Net461.References.MicrosoftVisualBasic
        ];
        public static ImmutableArray<MetadataReference> NetStandard20References { get; } =
        [
            NetStandard20.References.netstandard,
            NetStandard20.References.mscorlib,
            NetStandard20.References.SystemRuntime,
            NetStandard20.References.SystemCore,
            NetStandard20.References.SystemDynamicRuntime,
            NetStandard20.References.SystemLinq,
            NetStandard20.References.SystemLinqExpressions
        ];
        public static ImmutableArray<MetadataReference> DefaultVbReferences { get; } =
        [
            NetFramework.mscorlib,
            NetFramework.System,
            NetFramework.SystemCore,
            NetFramework.MicrosoftVisualBasic
        ];
 
#if DEBUG
 
        static TargetFrameworkUtil()
        {
            // Asserts to ensure these two values keep in sync
            Debug.Assert(GetReferences(TargetFramework.NetCoreApp).SequenceEqual(NetCoreApp.References));
            Debug.Assert(GetReferences(TargetFramework.NetFramework).SequenceEqual(NetFramework.References));
        }
 
#endif
 
        public static ImmutableArray<MetadataReference> GetReferences(TargetFramework targetFramework) => targetFramework switch
        {
            // Primary
            // Note: NetCoreApp should behave like latest Core TFM
            TargetFramework.Empty => ImmutableArray<MetadataReference>.Empty,
            TargetFramework.NetStandard20 => NetStandard20References,
            TargetFramework.Net50 => ImmutableArray.CreateRange<MetadataReference>(LoadDynamicReferences("Net50")),
            TargetFramework.Net60 => ImmutableArray.CreateRange<MetadataReference>(LoadDynamicReferences("Net60")),
            TargetFramework.NetCoreApp or TargetFramework.Net70 => ImmutableArray.CreateRange<MetadataReference>(Net70.References.All),
            TargetFramework.Net80 => ImmutableArray.CreateRange<MetadataReference>(LoadDynamicReferences("Net80")),
            TargetFramework.Net90 => ImmutableArray.CreateRange<MetadataReference>(LoadDynamicReferences("Net90")),
            TargetFramework.NetFramework => NetFramework.References,
            TargetFramework.NetLatest => NetLatest,
            TargetFramework.Standard => StandardReferences,
 
            // Legacy we should be phasing out
            TargetFramework.Mscorlib40 => Mscorlib40References,
            TargetFramework.Mscorlib40Extended => Mscorlib40ExtendedReferences,
            TargetFramework.Mscorlib40AndSystemCore => Mscorlib40andSystemCoreReferences,
            TargetFramework.Mscorlib40AndVBRuntime => Mscorlib40andVBRuntimeReferences,
            TargetFramework.Mscorlib46 => Mscorlib46References,
            TargetFramework.Mscorlib46Extended => Mscorlib46ExtendedReferences,
            TargetFramework.Mscorlib461 => Mscorlib46References,
            TargetFramework.Mscorlib461Extended => Mscorlib461ExtendedReferences,
            TargetFramework.Mscorlib461AndCSharp => Mscorlib461AndCSharpReferences,
            TargetFramework.Mscorlib461AndVBRuntime => Mscorlib461AndVBRuntimeReferences,
            TargetFramework.WinRT => WinRTReferences,
            TargetFramework.StandardAndCSharp => StandardAndCSharpReferences,
            TargetFramework.StandardAndVBRuntime => StandardAndVBRuntimeReferences,
            TargetFramework.DefaultVb => DefaultVbReferences,
            TargetFramework.Minimal => MinimalReferences,
            TargetFramework.MinimalAsync => MinimalAsyncReferences,
            _ => throw new InvalidOperationException($"Unexpected target framework {targetFramework}"),
        };
 
        public static ImmutableArray<MetadataReference> GetReferences(TargetFramework tf, IEnumerable<MetadataReference> additionalReferences)
        {
            var references = GetReferences(tf);
            if (additionalReferences == null)
            {
                return references;
            }
 
            checkForDuplicateReferences();
            return references.AddRange(additionalReferences);
 
            // Check to see if there are any duplicate references. This guards against tests inadvertently passing multiple copies of 
            // say System.Core to the tests and implicitly depending on the higher one to win. The few tests which actually mean to 
            // pass multiple versions of a DLL should manually construct the reference list and not use this helper.
            void checkForDuplicateReferences()
            {
                var nameSet = new HashSet<string>(getNames(references), StringComparer.OrdinalIgnoreCase);
                foreach (var r in additionalReferences)
                {
                    if (references.Contains(r))
                    {
                        throw new Exception($"Duplicate reference detected {r.Display}");
                    }
 
                    var name = getName(r);
                    if (name != null && !nameSet.Add(name))
                    {
                        throw new Exception($"Duplicate reference detected {r.Display} - {name}");
                    }
                }
            }
 
            IEnumerable<string> getNames(IEnumerable<MetadataReference> e)
            {
                foreach (var r in e)
                {
                    var name = getName(r);
                    if (name != null)
                    {
                        yield return name;
                    }
                }
            }
 
            string getName(MetadataReference m)
            {
                if (m is PortableExecutableReference p &&
                    p.GetMetadata() is AssemblyMetadata assemblyMetadata)
                {
                    try
                    {
                        var identity = assemblyMetadata.GetAssembly().Identity;
                        return identity?.Name;
                    }
                    catch (BadImageFormatException)
                    {
                        // Happens when a native image is incorrectly passed as a PE.
                        return null;
                    }
                }
 
                return null;
            }
        }
 
        public static IEnumerable<MetadataReference> GetReferencesWithout(TargetFramework targetFramework, params string[] excludeReferenceNames) =>
            GetReferences(targetFramework)
            .Where(x => !(x is PortableExecutableReference pe && excludeReferenceNames.Contains(pe.FilePath)));
 
        /// <summary>
        /// Many of our reference assemblies are only used by a subset of compiler unit tests. Having a PackageReference
        /// to the assemblies here would cause them to be deployed to every unit test we write though. These are non-trivial 
        /// in size (4+ MB) and we have ~50 test projects so this adds up fast. To keep size down we just add 
        /// PackageReference on the few projects that and dynamically load here.
        /// </summary>
        private static ImmutableArray<PortableExecutableReference> LoadDynamicReferences(string targetFrameworkName)
        {
            var assemblyName = $"Basic.Reference.Assemblies.{targetFrameworkName}";
            if (s_dynamicReferenceMap.TryGetValue(assemblyName, out var references))
            {
                return references;
            }
 
            try
            {
                var name = new AssemblyName(assemblyName);
                var assembly = Assembly.Load(name);
 
                var type = assembly.GetType(assemblyName, throwOnError: true);
                var prop = type.GetProperty("All", BindingFlags.Public | BindingFlags.Static);
                if (prop is null)
                {
                    type = assembly.GetType(assemblyName + "+References", throwOnError: true);
                    prop = type.GetProperty("All", BindingFlags.Public | BindingFlags.Static);
                }
                var obj = prop.GetGetMethod()!.Invoke(obj: null, parameters: null);
                references = ((IEnumerable<PortableExecutableReference>)obj).ToImmutableArray();
 
                // This method can de called in parallel. Who wins this TryAdd isn't important, it's the same 
                // values. 
                _ = s_dynamicReferenceMap.TryAdd(assemblyName, references);
                return references;
            }
            catch (Exception ex)
            {
                var message = $"Error loading {assemblyName}. Make sure the test project has a <PackageReference> for this assembly";
                throw new Exception(message, ex);
            }
        }
    }
}