File: Utilities\TypeExtensions.cs
Web Access
Project: src\msbuild\src\Framework\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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace Microsoft.Build.Shared;

internal static class TypeExtensions
{
    /// <summary>
    /// The member set a late-bound (property-function) receiver type must preserve for trimming.
    /// <c>InvokePublicMember</c> only ever binds members in this set, so preserving it is
    /// sufficient; annotating the receiver with it makes that requirement machine-checked at every
    /// call site, so a caller passing a type whose public surface is not preserved fails the build.
    /// </summary>
    private const DynamicallyAccessedMemberTypes PublicMemberSurface =
        DynamicallyAccessedMemberTypes.PublicConstructors |
        DynamicallyAccessedMemberTypes.PublicMethods |
        DynamicallyAccessedMemberTypes.PublicProperties |
        DynamicallyAccessedMemberTypes.PublicFields;

    extension(Type type)
    {
        [UnconditionalSuppressMessage("SingleFile", "IL3000",
            Justification = "Assembly.Location is empty under single-file/Native AOT; the empty result is handled here (and AOT hosts supply the MSBuild path via MSBUILD_EXE_PATH rather than relying on it).")]
        public string GetAssemblyPath()
        {
            // Path.GetFullPath throws on an empty string, which is exactly what Assembly.Location returns
            // for a single-file/Native AOT app, so return the empty path as-is in that case. In a hosted
            // (non-single-file) host Location is populated and this behaves as before.
            string location = type.Assembly.Location;
            return location.Length == 0 ? location : Path.GetFullPath(location);
        }

        /// <summary>
        /// Returns a boxed zero-initialized instance when the receiver is a value type, or
        /// <see langword="null"/> for a reference type. Used to seed an argument slot for a value-type
        /// <c>out</c> parameter so late-bound member resolution has a correctly typed box to write into.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        [UnconditionalSuppressMessage("Trimming", "IL2067",
            Justification = "Activator.CreateInstance is only invoked for value types (guarded by Type.IsValueType), which always have a public parameterless constructor.")]
        public object? CreateDefault()
            => type.IsValueType ? Activator.CreateInstance(type) : null;
    }

    extension([DynamicallyAccessedMembers(PublicMemberSurface)] Type type)
    {
        /// <summary>
        /// Invokes a public member on the receiver type using the standard binder, after verifying
        /// that <paramref name="bindingFlags"/> cannot reach non-public members.
        /// </summary>
        /// <remarks>
        /// This is the un-suppressed, public half of the pair. It owns both guarantees the trim suppression
        /// below depends on: the runtime guarantee (it throws if <see cref="BindingFlags.NonPublic"/> is set,
        /// so only public members can be bound) and the machine-checked guarantee (the
        /// <see cref="DynamicallyAccessedMembersAttribute"/> on the receiver, which the trim analyzer enforces
        /// at every call site). It delegates the single reflective call that needs a suppression to the private
        /// <c>InvokeMemberPublicOnly</c>, so that suppression cannot be reached except through this validated
        /// entry point and therefore cannot be misused or silently invalidated.
        /// </remarks>
        public object? InvokePublicMember(string name, BindingFlags bindingFlags, object? target, object?[]? args)
        {
            Assumed.Zero(
                (int)(bindingFlags & BindingFlags.NonPublic),
                $"'{BindingFlags.NonPublic}' is not permitted for {nameof(InvokePublicMember)}; only public members may be bound.");

            return InvokeMemberPublicOnly(type, name, bindingFlags, target, args);
        }
    }

    /// <summary>
    /// Performs the actual <see cref="Type.InvokeMember(string, BindingFlags, Binder, object, object[], CultureInfo)"/>
    /// call. This private method is the single, encapsulated home of the IL2070 suppression for property-function
    /// invocation; it must only ever be reached through <c>InvokePublicMember</c>.
    /// </summary>
    [UnconditionalSuppressMessage("Trimming", "IL2070",
        Justification = "Type.InvokeMember's reflection contract always lists non-public members, but the sole caller InvokePublicMember rejects BindingFlags.NonPublic (re-asserted below), so only public members are bound. The public surface of the receiver type is preserved for trimming by the [DynamicallyAccessedMembers] annotation on InvokePublicMember's receiver, which the trim analyzer enforces at every call site. Keeping this method private prevents the suppression from being bypassed or misused.")]
    private static object? InvokeMemberPublicOnly(
        [DynamicallyAccessedMembers(PublicMemberSurface)] Type type,
        string name,
        BindingFlags bindingFlags,
        object? target,
        object?[]? args)
    {
        Debug.Assert(
            (bindingFlags & BindingFlags.NonPublic) == 0,
            "InvokeMemberPublicOnly must never be called with BindingFlags.NonPublic; InvokePublicMember rejects it before delegating here.");
        return type.InvokeMember(name, bindingFlags, Type.DefaultBinder, target, args, CultureInfo.InvariantCulture);
    }
}