File: SignatureContext.cs
Web Access
Project: Microsoft.Interop.SourceGeneration.csproj (Microsoft.Interop.SourceGeneration)
// 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.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;

using Microsoft.CodeAnalysis;

namespace Microsoft.Interop
{
    public sealed record SignatureContext
    {
        // We don't need the warnings around not setting the various
        // non-nullable fields/properties on this type in the constructor
        // since we always use a property initializer.
#pragma warning disable 8618
        private SignatureContext()
        {
        }
#pragma warning restore

        public ImmutableArray<TypePositionInfo> ElementTypeInformation { get; init; }

        public IEnumerable<TypePositionInfo> ManagedParameters => ElementTypeInformation.Where(tpi => !TypePositionInfo.IsSpecialIndex(tpi.ManagedIndex));

        public string StubReturnType { get; init; }

        public IEnumerable<GeneratedParameter> StubParameters
        {
            get
            {
                foreach (TypePositionInfo typeInfo in ElementTypeInformation)
                {
                    if (!TypePositionInfo.IsSpecialIndex(typeInfo.ManagedIndex))
                    {
                        yield return new GeneratedParameter(
                            typeInfo.ManagedType.FullTypeName,
                            typeInfo.InstanceIdentifier,
                            MarshallerHelpers.GetManagedParameterModifiers(typeInfo));
                    }
                }
            }
        }

        public ImmutableArray<string> AdditionalAttributes { get; init; }

        public static SignatureContext Create(
            IMethodSymbol method,
            MarshallingInfoParser marshallingInfoParser,
            StubEnvironment env,
            CodeEmitOptions options,
            Assembly generatorInfoAssembly)
        {
            return Create(method, marshallingInfoParser, env, options, generatorInfoAssembly, errorHandlingInfo: null);
        }

        public static SignatureContext Create(
            IMethodSymbol method,
            MarshallingInfoParser marshallingInfoParser,
            StubEnvironment env,
            CodeEmitOptions options,
            Assembly generatorInfoAssembly,
            ErrorHandlingInfo? errorHandlingInfo)
        {
            ImmutableArray<TypePositionInfo> typeInfos = GenerateTypeInformation(method, marshallingInfoParser, env, errorHandlingInfo);

            ImmutableArray<string>.Builder additionalAttrs = ImmutableArray.CreateBuilder<string>();

            string generatorName = generatorInfoAssembly.GetName().Name;
            string generatorVersion = generatorInfoAssembly.GetName().Version.ToString();
            // Define additional attributes for the stub definition.
            additionalAttrs.Add($"{TypeNames.GlobalAlias}{TypeNames.System_CodeDom_Compiler_GeneratedCodeAttribute}({CodeWriterHelpers.StringLiteral(generatorName)}, {CodeWriterHelpers.StringLiteral(generatorVersion)})");

            if (options.SkipInit && !MethodIsSkipLocalsInit(env, method))
            {
                additionalAttrs.Add(TypeNames.GlobalAlias + TypeNames.System_Runtime_CompilerServices_SkipLocalsInitAttribute);
            }

            return new SignatureContext()
            {
                StubReturnType = method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
                ElementTypeInformation = typeInfos,
                AdditionalAttributes = additionalAttrs.ToImmutable(),
            };
        }

        private static ImmutableArray<TypePositionInfo> GenerateTypeInformation(
            IMethodSymbol method,
            MarshallingInfoParser marshallingInfoParser,
            StubEnvironment env,
            ErrorHandlingInfo? errorHandlingInfo)
        {
            // When the underlying method is a property accessor, bare attributes on the property declaration
            // (e.g. `[MarshalUsing(typeof(X))] string Prop { get; set; }`) land on the property symbol and
            // are not otherwise visible to the marshalling pipeline. Fall them through to the accessor's
            // value surface only -- the getter's return, or the setter's value parameter (the last
            // parameter, after any indexer index parameters). Index parameters on indexer accessors and the
            // setter's `void` return are not value surfaces and do not inherit property-level attributes.
            // Accessor-level attributes win over property-level ones on a per-type basis. Target-scoped
            // attributes (`[return:]`, `[param:]`, `[get:]`, `[set:]`) are routed by Roslyn onto the
            // accessor directly and so are already in the accessor's attribute set.
            ImmutableArray<AttributeData> associatedPropertyAttributes = method.AssociatedSymbol is IPropertySymbol property
                ? property.GetAttributes()
                : ImmutableArray<AttributeData>.Empty;

            // The value parameter on a setter is the last parameter (index parameters precede it on
            // indexer setters). Getters have no value parameter -- their value surface is the return.
            int valueParameterIndex = method.MethodKind == MethodKind.PropertySet
                ? method.Parameters.Length - 1
                : -1;

            ImmutableArray<TypePositionInfo>.Builder typeInfos = ImmutableArray.CreateBuilder<TypePositionInfo>();
            for (int i = 0; i < method.Parameters.Length; i++)
            {
                IParameterSymbol param = method.Parameters[i];
                ImmutableArray<AttributeData> paramAttributes = i == valueParameterIndex
                    ? MergeAccessorAndPropertyAttributes(param.GetAttributes(), associatedPropertyAttributes)
                    : param.GetAttributes();
                MarshallingInfo marshallingInfo = marshallingInfoParser.ParseMarshallingInfo(param.Type, paramAttributes);
                var typeInfo = TypePositionInfo.CreateForParameter(param, marshallingInfo, env.Compilation);
                typeInfo = typeInfo with
                {
                    ManagedIndex = i,
                    NativeIndex = typeInfos.Count
                };
                typeInfos.Add(typeInfo);
            }

            ImmutableArray<AttributeData> returnAttributes = method.MethodKind == MethodKind.PropertyGet
                ? MergeAccessorAndPropertyAttributes(method.GetReturnTypeAttributes(), associatedPropertyAttributes)
                : method.GetReturnTypeAttributes();
            TypePositionInfo retTypeInfo = new(ManagedTypeInfo.CreateTypeInfoForTypeSymbol(method.ReturnType), marshallingInfoParser.ParseMarshallingInfo(method.ReturnType, returnAttributes));
            retTypeInfo = retTypeInfo with
            {
                ManagedIndex = TypePositionInfo.ReturnIndex,
                NativeIndex = TypePositionInfo.ReturnIndex,
            };

            typeInfos.Add(retTypeInfo);

            if (errorHandlingInfo is not null)
            {
                ApplyErrorHandlingInfo(typeInfos, errorHandlingInfo);
            }

            return typeInfos.ToImmutable();

            void ApplyErrorHandlingInfo(ImmutableArray<TypePositionInfo>.Builder infos, ErrorHandlingInfo errorInfo)
            {
                TypePositionInfo CreateErrorInfo(
                    int nativeIndex)
                {
                    return new TypePositionInfo(errorInfo.ManagedType, errorInfo.MarshallingInfo)
                    {
                        InstanceIdentifier = "__error",
                        RefKind = nativeIndex == TypePositionInfo.ReturnIndex ? RefKind.None : RefKind.Out,
                        ManagedIndex = TypePositionInfo.ErrorIndex,
                        NativeIndex = nativeIndex,
                        IsErrorHandlingPosition = true,
                    };
                }

                bool MatchesManagedType(TypePositionInfo info) => info.ManagedType == errorInfo.ManagedType;

                switch (errorInfo.Location)
                {
                    case ErrorHandlingLocation.ReturnValue:
                        int returnIndex = infos.Count - 1;
                        TypePositionInfo returnInfo = infos[returnIndex];
                        if (MatchesManagedType(returnInfo))
                        {
                            infos.Add(CreateErrorInfo(TypePositionInfo.ReturnIndex));
                        }
                        else if (returnInfo.ManagedType == SpecialTypeInfo.Void)
                        {
                            infos[returnIndex] = returnInfo with { NativeIndex = TypePositionInfo.UnsetIndex };
                            infos.Add(CreateErrorInfo(TypePositionInfo.ReturnIndex));
                        }
                        break;

                    case ErrorHandlingLocation.HiddenReturnValue:
                        int hiddenReturnIndex = infos.Count - 1;
                        TypePositionInfo hiddenReturnInfo = infos[hiddenReturnIndex];
                        if (hiddenReturnInfo.ManagedType == SpecialTypeInfo.Void)
                        {
                            infos[hiddenReturnIndex] = hiddenReturnInfo with { NativeIndex = TypePositionInfo.UnsetIndex };
                        }
                        else
                        {
                            // Match the COM ABI transformation: keep the value in the managed return
                            // position while moving it to a final out parameter in the native signature.
                            infos[hiddenReturnIndex] = hiddenReturnInfo with
                            {
                                RefKind = RefKind.Out,
                                NativeIndex = method.Parameters.Length,
                            };
                        }

                        infos.Add(CreateErrorInfo(TypePositionInfo.ReturnIndex));
                        break;

                    case ErrorHandlingLocation.LastParameter:
                        TypePositionInfo lastParameter = GetLastManagedParameter(infos);
                        Debug.Assert(lastParameter is { RefKind: RefKind.Out or RefKind.Ref }
                            && MatchesManagedType(lastParameter));
                        infos.Add(CreateErrorInfo(lastParameter.NativeIndex));
                        break;

                    case ErrorHandlingLocation.HiddenLastParameter:
                        infos.Add(CreateErrorInfo(method.Parameters.Length));
                        break;

                }

                static TypePositionInfo GetLastManagedParameter(ImmutableArray<TypePositionInfo>.Builder infos)
                {
                    TypePositionInfo? lastParameter = null;
                    foreach (TypePositionInfo info in infos)
                    {
                        if (!TypePositionInfo.IsSpecialIndex(info.ManagedIndex)
                            && (lastParameter is null || info.ManagedIndex > lastParameter.ManagedIndex))
                        {
                            lastParameter = info;
                        }
                    }

                    return lastParameter ?? throw new UnreachableException();
                }
            }
        }

        private static ImmutableArray<AttributeData> MergeAccessorAndPropertyAttributes(
            ImmutableArray<AttributeData> accessorAttributes,
            ImmutableArray<AttributeData> associatedPropertyAttributes)
        {
            if (associatedPropertyAttributes.IsEmpty)
            {
                return accessorAttributes;
            }

            // Accessor-level attributes win over property-level ones at the same dedup key
            // (attribute type + ElementIndirectionDepth for [MarshalUsing], attribute type alone
            // otherwise). [MarshalUsing] is the only AllowMultiple = true attribute that flows
            // through this merge: it can repeat on a single value surface with distinct
            // ElementIndirectionDepth values to describe marshalling at successive levels of
            // indirection (the value itself at depth 0, its elements at depth 1, and so on). The
            // public contract on MarshalUsingAttribute.ElementIndirectionDepth states only one
            // [MarshalUsing] with a given depth may be provided on a given parameter or return
            // value, so dedup keys for [MarshalUsing] include the depth -- an accessor-level
            // [MarshalUsing] overrides only the property-level [MarshalUsing] at the matching
            // depth, and property-level [MarshalUsing]s at other depths flow through.
            //
            // To keep this dedup unambiguous, the COM generator additionally rejects accessor-level
            // [MarshalUsing] attributes that omit the marshaller type (see
            // MarshalUsingOnPropertyAccessorMustSpecifyType in GeneratorDiagnostics). That keeps the
            // partial-split case (e.g., marshaller type on the property and count-only on the
            // accessor) from silently dropping one side; the user combines the information on a
            // single attribute or attaches the count-only [MarshalUsing] to the property.
            HashSet<(string?, int)> accessorAttributeKeys = new();
            foreach (AttributeData attr in accessorAttributes)
            {
                accessorAttributeKeys.Add(GetMergeKey(attr));
            }

            ImmutableArray<AttributeData>.Builder merged = ImmutableArray.CreateBuilder<AttributeData>(accessorAttributes.Length + associatedPropertyAttributes.Length);
            merged.AddRange(accessorAttributes);
            foreach (AttributeData attr in associatedPropertyAttributes)
            {
                if (!accessorAttributeKeys.Contains(GetMergeKey(attr)))
                {
                    merged.Add(attr);
                }
            }
            return merged.ToImmutable();

            static (string?, int) GetMergeKey(AttributeData attr)
            {
                string? attributeName = attr.AttributeClass?.ToDisplayString();
                int depth = 0;
                if (attributeName == TypeNames.MarshalUsingAttribute)
                {
                    foreach (KeyValuePair<string, TypedConstant> named in attr.NamedArguments)
                    {
                        if (named.Key == ManualTypeMarshallingHelper.MarshalUsingProperties.ElementIndirectionDepth)
                        {
                            depth = (int)named.Value.Value!;
                            break;
                        }
                    }
                }
                return (attributeName, depth);
            }
        }

        public bool Equals(SignatureContext other)
        {
            // We don't check if the generator factories are equal since
            // the generator factory is deterministically created based on the ElementTypeInformation and Options.
            return other is not null
                && ElementTypeInformation.SequenceEqual(other.ElementTypeInformation)
                && StubReturnType == other.StubReturnType
                && AdditionalAttributes.SequenceEqual(other.AdditionalAttributes);
        }

        public override int GetHashCode()
        {
            throw new UnreachableException();
        }

        private static bool MethodIsSkipLocalsInit(StubEnvironment env, IMethodSymbol method)
        {
            if (env.EnvironmentFlags.HasFlag(EnvironmentFlags.SkipLocalsInit))
            {
                return true;
            }

            if (method.GetAttributes().Any(IsSkipLocalsInitAttribute))
            {
                return true;
            }

            for (INamedTypeSymbol type = method.ContainingType; type is not null; type = type.ContainingType)
            {
                if (type.GetAttributes().Any(IsSkipLocalsInitAttribute))
                {
                    return true;
                }
            }

            // We check the module case earlier, so we don't need to do it here.
            return false;

            static bool IsSkipLocalsInitAttribute(AttributeData a)
                => a.AttributeClass?.ToDisplayString() == TypeNames.System_Runtime_CompilerServices_SkipLocalsInitAttribute;
        }
    }
}