File: JSManagedTypeInfo.cs
Web Access
Project: JSImportGenerator.csproj (Microsoft.Interop.JavaScript.JSImportGenerator)
// 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.Linq;
using Microsoft.CodeAnalysis;

namespace Microsoft.Interop.JavaScript
{
    internal abstract record JSTypeInfo(KnownManagedType KnownType)
    {
        public static JSTypeInfo CreateJSTypeInfoForTypeSymbol(ITypeSymbol type)
        {
            string fullTypeName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
            switch (type)
            {
                case { SpecialType: SpecialType.System_Void }:
                    return new JSSimpleTypeInfo(KnownManagedType.Void, "void");
                case { SpecialType: SpecialType.System_Boolean }:
                    return new JSSimpleTypeInfo(KnownManagedType.Boolean, "bool");
                case { SpecialType: SpecialType.System_Byte }:
                    return new JSSimpleTypeInfo(KnownManagedType.Byte, "byte");
                case { SpecialType: SpecialType.System_Char }:
                    return new JSSimpleTypeInfo(KnownManagedType.Char, "char");
                case { SpecialType: SpecialType.System_Int16 }:
                    return new JSSimpleTypeInfo(KnownManagedType.Int16, "short");
                case { SpecialType: SpecialType.System_Int32 }:
                    return new JSSimpleTypeInfo(KnownManagedType.Int32, "int");
                case { SpecialType: SpecialType.System_Int64 }:
                    return new JSSimpleTypeInfo(KnownManagedType.Int64, "long");
                case { SpecialType: SpecialType.System_Single }:
                    return new JSSimpleTypeInfo(KnownManagedType.Single, "float");
                case { SpecialType: SpecialType.System_Double }:
                    return new JSSimpleTypeInfo(KnownManagedType.Double, "double");
                case { SpecialType: SpecialType.System_IntPtr }:
                case IPointerTypeSymbol { PointedAtType.SpecialType: SpecialType.System_Void }:
                    return new JSSimpleTypeInfo(KnownManagedType.IntPtr, "nint");
                case { SpecialType: SpecialType.System_DateTime }:
                    return new JSSimpleTypeInfo(KnownManagedType.DateTime, fullTypeName);
                case ITypeSymbol when fullTypeName == "global::System.DateTimeOffset":
                    return new JSSimpleTypeInfo(KnownManagedType.DateTimeOffset, fullTypeName);
                case ITypeSymbol when fullTypeName == "global::System.Exception":
                    return new JSSimpleTypeInfo(KnownManagedType.Exception, fullTypeName);
                case { SpecialType: SpecialType.System_Object }:
                    return new JSSimpleTypeInfo(KnownManagedType.Object, "object");
                case { SpecialType: SpecialType.System_String }:
                    return new JSSimpleTypeInfo(KnownManagedType.String, "string");
                case ITypeSymbol when fullTypeName == "global::System.Runtime.InteropServices.JavaScript.JSObject":
                    return new JSSimpleTypeInfo(KnownManagedType.JSObject, fullTypeName);

                //nullable
                case INamedTypeSymbol { ConstructedFrom.SpecialType: SpecialType.System_Nullable_T } nullable:
                    if (CreateJSTypeInfoForTypeSymbol(nullable.TypeArguments[0]) is JSSimpleTypeInfo uti)
                    {
                        return new JSNullableTypeInfo(uti);
                    }
                    return new JSInvalidTypeInfo();

                // array
                case IArrayTypeSymbol { IsSZArray: true, ElementType: ITypeSymbol elementType }:
                    if (CreateJSTypeInfoForTypeSymbol(elementType) is JSSimpleTypeInfo eti)
                    {
                        return new JSArrayTypeInfo(eti);
                    }
                    return new JSInvalidTypeInfo();

                // task
                case ITypeSymbol when fullTypeName == Constants.TaskGlobal:
                    return new JSTaskTypeInfo(new JSSimpleTypeInfo(KnownManagedType.Void, "void"));
                case INamedTypeSymbol { TypeArguments.Length: 1 } taskType when fullTypeName.StartsWith(Constants.TaskGlobal, StringComparison.Ordinal):
                    if (CreateJSTypeInfoForTypeSymbol(taskType.TypeArguments[0]) is JSSimpleTypeInfo rti)
                    {
                        return new JSTaskTypeInfo(rti);
                    }
                    return new JSInvalidTypeInfo();

                // span
                case INamedTypeSymbol { TypeArguments.Length: 1 } spanType when fullTypeName.StartsWith(Constants.SpanGlobal, StringComparison.Ordinal):
                    if (CreateJSTypeInfoForTypeSymbol(spanType.TypeArguments[0]) is JSSimpleTypeInfo sti)
                    {
                        return new JSSpanTypeInfo(sti);
                    }
                    return new JSInvalidTypeInfo();

                // array segment
                case INamedTypeSymbol { TypeArguments.Length: 1 } arraySegmentType when fullTypeName.StartsWith(Constants.ArraySegmentGlobal, StringComparison.Ordinal):
                    if (CreateJSTypeInfoForTypeSymbol(arraySegmentType.TypeArguments[0]) is JSSimpleTypeInfo gti)
                    {
                        return new JSArraySegmentTypeInfo(gti);
                    }
                    return new JSInvalidTypeInfo();

                // action
                case ITypeSymbol when fullTypeName == Constants.ActionGlobal:
                    return new JSFunctionTypeInfo(true, Array.Empty<JSSimpleTypeInfo>());
                case INamedTypeSymbol actionType when fullTypeName.StartsWith(Constants.ActionGlobal, StringComparison.Ordinal):
                    var argumentTypes = actionType.TypeArguments
                        .Select(arg => CreateJSTypeInfoForTypeSymbol(arg) as JSSimpleTypeInfo)
                        .ToArray();
                    if (argumentTypes.Any(x => x is null))
                    {
                        return new JSInvalidTypeInfo();
                    }
                    return new JSFunctionTypeInfo(true, argumentTypes);

                // function
                case INamedTypeSymbol funcType when fullTypeName.StartsWith(Constants.FuncGlobal, StringComparison.Ordinal):
                    var signatureTypes = funcType.TypeArguments
                        .Select(argName => CreateJSTypeInfoForTypeSymbol(argName) as JSSimpleTypeInfo)
                        .ToArray();
                    if (signatureTypes.Any(x => x is null))
                    {
                        return new JSInvalidTypeInfo();
                    }
                    return new JSFunctionTypeInfo(false, signatureTypes);
                default:
                    // JS Interop generator does not support the marshalling of structs
                    // In case structs were to be allowed for marshalling in the future,
                    // disallow marshalling of structs with the InlineArrayAttribute
                    return new JSInvalidTypeInfo();
            }
        }
    }

    internal sealed record JSInvalidTypeInfo() : JSSimpleTypeInfo(KnownManagedType.None, "");

    internal record JSSimpleTypeInfo(KnownManagedType KnownType, string FullTypeName) : JSTypeInfo(KnownType);

    internal sealed record JSArrayTypeInfo(JSSimpleTypeInfo ElementTypeInfo) : JSTypeInfo(KnownManagedType.Array);

    internal sealed record JSSpanTypeInfo(JSSimpleTypeInfo ElementTypeInfo) : JSTypeInfo(KnownManagedType.Span);

    internal sealed record JSArraySegmentTypeInfo(JSSimpleTypeInfo ElementTypeInfo) : JSTypeInfo(KnownManagedType.ArraySegment);

    internal sealed record JSTaskTypeInfo(JSSimpleTypeInfo ResultTypeInfo) : JSTypeInfo(KnownManagedType.Task);

    internal sealed record JSNullableTypeInfo(JSSimpleTypeInfo ResultTypeInfo) : JSTypeInfo(KnownManagedType.Nullable);

    internal sealed record JSFunctionTypeInfo(bool IsAction, JSSimpleTypeInfo[] ArgsTypeInfo) : JSTypeInfo(IsAction ? KnownManagedType.Action : KnownManagedType.Function)
    {
        public bool Equals(JSFunctionTypeInfo? other)
        {
            return other is not null
                && IsAction == other.IsAction
                && ArgsTypeInfo.SequenceEqual(other.ArgsTypeInfo);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                int hash = IsAction.GetHashCode();
                foreach (JSSimpleTypeInfo argument in ArgsTypeInfo)
                {
                    hash = hash * 31 + argument.GetHashCode();
                }
                return hash;
            }
        }
    }
}