From cf2e7d328f47ea5f70f45aa51f83be71cb2f7952 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 19 Apr 2026 21:29:57 +0300 Subject: [PATCH 1/3] iter --- .../Emit/InterfacesTest.cs | 150 +++++------ .../Emit/InteropTest.cs | 84 +++++- .../Emit/SerializerTest.cs | 27 +- .../Pack/BindingTest.cs | 131 +++++++++- .../Pack/DeclarationTest.cs | 99 +++++++- .../Common/Global/GlobalType.cs | 20 +- .../Common/Meta/InterfaceMeta.cs | 20 +- .../Common/Meta/InteropKind.cs | 16 ++ .../Common/Meta/MemberMeta.cs | 93 +++++++ .../Common/Meta/MethodMeta.cs | 78 ------ .../SolutionInspector/InspectionReporter.cs | 16 +- .../SolutionInspector/InterfaceInspector.cs | 52 ++-- .../SolutionInspector/MemberInspector.cs | 59 +++++ .../SolutionInspector/MethodInspector.cs | 58 ----- .../SolutionInspector/SerializedInspector.cs | 7 +- .../SolutionInspector/SolutionInspection.cs | 4 +- .../SolutionInspector/SolutionInspector.cs | 78 +++--- .../Common/SolutionInspector/TypeInspector.cs | 6 +- .../Emit/DependencyGenerator.cs | 2 +- .../Emit/InterfaceGenerator.cs | 125 ++++++--- .../Emit/InteropGenerator.cs | 239 +++++++++++------- .../Emit/InteropInitializerGenerator.cs | 4 +- .../BindingGenerator/BindingClassGenerator.cs | 29 ++- .../Pack/BindingGenerator/BindingGenerator.cs | 203 +++++++++------ .../DeclarationGenerator.cs | 4 +- .../MemberDeclarationGenerator.cs | 90 +++++++ .../MethodDeclarationGenerator.cs | 78 ------ .../TypeDeclarationGenerator.cs | 59 +++-- .../DeclarationGenerator/TypeSyntaxBuilder.cs | 4 +- src/cs/Directory.Build.props | 2 +- .../Interfaces/ExportedInstanced.cs | 6 +- .../Test.Types/Interfaces/ExportedStatic.cs | 2 + .../Interfaces/IExportedInstanced.cs | 3 +- .../Test.Types/Interfaces/IExportedStatic.cs | 1 + .../Interfaces/IImportedInstanced.cs | 3 +- .../Test.Types/Interfaces/IImportedStatic.cs | 1 + .../cs/Test.Types/Interfaces/Interfaces.cs | 53 ++++ src/js/test/cs/Test.Types/Record.cs | 3 + src/js/test/cs/Test.Types/Vehicle/Registry.cs | 3 + src/js/test/cs/Test/Program.cs | 32 --- src/js/test/cs/Test/Serialization.cs | 9 + src/js/test/spec/interop.spec.ts | 37 ++- src/js/test/spec/serialization.spec.ts | 9 + 43 files changed, 1289 insertions(+), 710 deletions(-) create mode 100644 src/cs/Bootsharp.Publish/Common/Meta/InteropKind.cs create mode 100644 src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs delete mode 100644 src/cs/Bootsharp.Publish/Common/Meta/MethodMeta.cs create mode 100644 src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs delete mode 100644 src/cs/Bootsharp.Publish/Common/SolutionInspector/MethodInspector.cs create mode 100644 src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs delete mode 100644 src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MethodDeclarationGenerator.cs create mode 100644 src/js/test/cs/Test.Types/Interfaces/Interfaces.cs create mode 100644 src/js/test/cs/Test.Types/Record.cs diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs index d08214a2..7e42bcc2 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs @@ -15,6 +15,8 @@ public record Record; public interface IExported { + Record? Record { get; set; } + void Inv (string? a); Task InvAsync (); Record? InvRecord (); @@ -25,6 +27,18 @@ public interface IExported Execute(); Contains( """ + namespace Bootsharp.Generated + { + internal static class InterfaceRegistrations + { + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterInterfaces () + { + Interfaces.Register(typeof(Bootsharp.Generated.Exports.JSExported), new ExportInterface(typeof(global::IExported), handler => new Bootsharp.Generated.Exports.JSExported((global::IExported)handler))); + } + } + } + namespace Bootsharp.Generated.Exports { public class JSExported @@ -36,6 +50,8 @@ public JSExported (global::IExported handler) JSExported.handler = handler; } + [JSInvokable] public static global::Record? GetPropertyRecord () => handler.Record; + [JSInvokable] public static void SetPropertyRecord (global::Record? value) => handler.Record = value; [JSInvokable] public static void Inv (global::System.String? a) => handler.Inv(a); [JSInvokable] public static global::System.Threading.Tasks.Task InvAsync () => handler.InvAsync(); [JSInvokable] public static global::Record? InvRecord () => handler.InvRecord(); @@ -44,20 +60,6 @@ public JSExported (global::IExported handler) } } """); - Contains( - """ - namespace Bootsharp.Generated - { - internal static class InterfaceRegistrations - { - [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterInterfaces () - { - Interfaces.Register(typeof(Bootsharp.Generated.Exports.JSExported), new ExportInterface(typeof(global::IExported), handler => new Bootsharp.Generated.Exports.JSExported((global::IExported)handler))); - } - } - } - """); } [Fact] @@ -71,6 +73,8 @@ public record Record; public interface IImported { + Record? Record { get; set; } + void Inv (string? a); Task InvAsync (); Record? InvRecord (); @@ -79,26 +83,6 @@ public interface IImported } """)); Execute(); - Contains( - """ - namespace Bootsharp.Generated.Imports - { - public class JSImported : global::IImported - { - [JSFunction] public static void Inv (global::System.String? a) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_Inv(a); - [JSFunction] public static global::System.Threading.Tasks.Task InvAsync () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvAsync(); - [JSFunction] public static global::Record? InvRecord () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvRecord(); - [JSFunction] public static global::System.Threading.Tasks.Task InvAsyncResult () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvAsyncResult(); - [JSFunction] public static global::System.String[] InvArray (global::System.Int32[] a) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvArray(a); - - void global::IImported.Inv (global::System.String? a) => Inv(a); - global::System.Threading.Tasks.Task global::IImported.InvAsync () => InvAsync(); - global::Record? global::IImported.InvRecord () => InvRecord(); - global::System.Threading.Tasks.Task global::IImported.InvAsyncResult () => InvAsyncResult(); - global::System.String[] global::IImported.InvArray (global::System.Int32[] a) => InvArray(a); - } - } - """); Contains( """ namespace Bootsharp.Generated @@ -112,6 +96,23 @@ internal static void RegisterInterfaces () } } } + + namespace Bootsharp.Generated.Imports + { + public class JSImported : global::IImported + { + global::Record? global::IImported.Record + { + get => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_GetPropertyRecord(); + set => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_SetPropertyRecord(value); + } + void global::IImported.Inv (global::System.String? a) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_Inv(a); + global::System.Threading.Tasks.Task global::IImported.InvAsync () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvAsync(); + global::Record? global::IImported.InvRecord () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvRecord(); + global::System.Threading.Tasks.Task global::IImported.InvAsyncResult () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvAsyncResult(); + global::System.String[] global::IImported.InvArray (global::System.Int32[] a) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvArray(a); + } + } """); } @@ -120,8 +121,15 @@ public void GeneratesImplementationForInstancedImportInterface () { AddAssembly(With( """ + public record Record; public interface IExported { void Inv (string arg); } - public interface IImported { void Fun (string arg); void NotifyEvt(string arg); } + public interface IImported + { + Record? Record { get; set; } + + void Fun (string arg); + void NotifyEvt (string arg); + } public class Class { @@ -138,11 +146,13 @@ public class JSImported(global::System.Int32 _id) : global::IImported { ~JSImported() => global::Bootsharp.Generated.Interop.DisposeImportedInstance(_id); - [JSFunction] public static void Fun (global::System.Int32 _id, global::System.String arg) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_Fun(_id, arg); - [JSEvent] public static void OnEvt (global::System.Int32 _id, global::System.String arg) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_OnEvt(_id, arg); - - void global::IImported.Fun (global::System.String arg) => Fun(_id, arg); - void global::IImported.NotifyEvt (global::System.String arg) => OnEvt(_id, arg); + global::Record? global::IImported.Record + { + get => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_GetPropertyRecord(_id); + set => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_SetPropertyRecord(_id, value); + } + void global::IImported.Fun (global::System.String arg) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_Fun(_id, arg); + void global::IImported.NotifyEvt (global::System.String arg) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_OnEvt(_id, arg); } } """); @@ -167,6 +177,19 @@ public interface IImported { void Fun (Record a); } Execute(); Contains( """ + namespace Bootsharp.Generated + { + internal static class InterfaceRegistrations + { + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterInterfaces () + { + Interfaces.Register(typeof(Bootsharp.Generated.Exports.Space.JSExported), new ExportInterface(typeof(global::Space.IExported), handler => new Bootsharp.Generated.Exports.Space.JSExported((global::Space.IExported)handler))); + Interfaces.Register(typeof(global::Space.IImported), new ImportInterface(new Bootsharp.Generated.Imports.Space.JSImported())); + } + } + } + namespace Bootsharp.Generated.Exports.Space { public class JSExported @@ -181,28 +204,12 @@ public JSExported (global::Space.IExported handler) [JSInvokable] public static void Inv (global::Space.Record a) => handler.Inv(a); } } + namespace Bootsharp.Generated.Imports.Space { public class JSImported : global::Space.IImported { - [JSFunction] public static void Fun (global::Space.Record a) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_Space_JSImported_Fun(a); - - void global::Space.IImported.Fun (global::Space.Record a) => Fun(a); - } - } - """); - Contains( - """ - namespace Bootsharp.Generated - { - internal static class InterfaceRegistrations - { - [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterInterfaces () - { - Interfaces.Register(typeof(Bootsharp.Generated.Exports.Space.JSExported), new ExportInterface(typeof(global::Space.IExported), handler => new Bootsharp.Generated.Exports.Space.JSExported((global::Space.IExported)handler))); - Interfaces.Register(typeof(global::Space.IImported), new ImportInterface(new Bootsharp.Generated.Imports.Space.JSImported())); - } + void global::Space.IImported.Fun (global::Space.Record a) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_Space_JSImported_Fun(a); } } """); @@ -224,9 +231,7 @@ namespace Bootsharp.Generated.Imports { public class JSImported : global::IImported { - [JSEvent] public static void OnFoo () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_OnFoo(); - - void global::IImported.NotifyFoo () => OnFoo(); + void global::IImported.NotifyFoo () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_OnFoo(); } } """); @@ -247,20 +252,6 @@ public interface IImported } """)); Execute(); - Contains( - """ - namespace Bootsharp.Generated.Imports - { - public class JSImported : global::IImported - { - [JSFunction] public static void NotifyFoo () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_NotifyFoo(); - [JSEvent] public static void OnBar () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_OnBar(); - - void global::IImported.NotifyFoo () => NotifyFoo(); - void global::IImported.BroadcastBar () => OnBar(); - } - } - """); Contains( """ namespace Bootsharp.Generated @@ -274,6 +265,15 @@ internal static void RegisterInterfaces () } } } + + namespace Bootsharp.Generated.Imports + { + public class JSImported : global::IImported + { + void global::IImported.NotifyFoo () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_NotifyFoo(); + void global::IImported.BroadcastBar () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_OnBar(); + } + } """); } diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs index 62148813..52da4c9d 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs @@ -209,9 +209,9 @@ public class Class Contains("JSExport] internal static global::System.Threading.Tasks.Task Space_Class_Inv (global::System.Boolean a1, global::System.Byte a2, global::System.Char a3, global::System.Int16 a4, [JSMarshalAs] global::System.Int64 a5, global::System.Int32 a6, global::System.Single a7, global::System.Double a8, global::System.IntPtr a9, [JSMarshalAs] global::System.DateTime a10, [JSMarshalAs] global::System.DateTimeOffset a11, global::System.String a12) => global::Space.Class.Inv(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); Contains("JSExport] [return: JSMarshalAs>] internal static global::System.Threading.Tasks.Task Space_Class_InvNull (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, [JSMarshalAs] global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, [JSMarshalAs] global::System.DateTime? a10, [JSMarshalAs] global::System.DateTimeOffset? a11, global::System.String? a12) => global::Space.Class.InvNull(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); Contains("""JSImport("Space.Class.funSerialized", "Bootsharp")] internal static partial global::System.Threading.Tasks.Task Space_Class_Fun (global::System.Boolean a1, global::System.Byte a2, global::System.Char a3, global::System.Int16 a4, [JSMarshalAs] global::System.Int64 a5, global::System.Int32 a6, global::System.Single a7, global::System.Double a8, global::System.IntPtr a9, [JSMarshalAs] global::System.DateTime a10, [JSMarshalAs] global::System.DateTimeOffset a11, global::System.String a12);"""); - Contains("public static global::System.Threading.Tasks.Task Proxy_Space_Class_FunNull(global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, global::System.DateTime? a10, global::System.DateTimeOffset? a11, global::System.String? a12) => Space_Class_FunNull(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); + Contains("public static global::System.Threading.Tasks.Task Proxy_Space_Class_FunNull (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, global::System.DateTime? a10, global::System.DateTimeOffset? a11, global::System.String? a12) => Space_Class_FunNull(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); Contains("""JSImport("Space.Class.funNullSerialized", "Bootsharp")] [return: JSMarshalAs>] internal static partial global::System.Threading.Tasks.Task Space_Class_FunNull (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, [JSMarshalAs] global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, [JSMarshalAs] global::System.DateTime? a10, [JSMarshalAs] global::System.DateTimeOffset? a11, global::System.String? a12);"""); - Contains("public static global::System.Threading.Tasks.Task Proxy_Space_Class_FunNull(global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, global::System.DateTime? a10, global::System.DateTimeOffset? a11, global::System.String? a12) => Space_Class_FunNull(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); + Contains("public static global::System.Threading.Tasks.Task Proxy_Space_Class_FunNull (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, global::System.DateTime? a10, global::System.DateTimeOffset? a11, global::System.String? a12) => Space_Class_FunNull(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); } [Fact] @@ -235,9 +235,9 @@ public class Class Contains("JSExport] [return: JSMarshalAs] internal static global::System.Int64 Space_Class_InvA ([JSMarshalAs] global::System.Int64 a) => Serializer.Serialize(global::Space.Class.InvA(Serializer.Deserialize(a, SerializerContext.Space_Record)), SerializerContext.Space_Record);"); Contains("JSExport] [return: JSMarshalAs>] internal static async global::System.Threading.Tasks.Task Space_Class_InvB ([JSMarshalAs] global::System.Int64 a) => Serializer.Serialize(await global::Space.Class.InvB(Serializer.Deserialize(a, SerializerContext.Space_RecordArray)), SerializerContext.Space_RecordArray);"); Contains("""JSImport("Space.Class.funASerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Space_Class_FunA ([JSMarshalAs] global::System.Int64 a);"""); - Contains("public static global::Space.Record Proxy_Space_Class_FunA(global::Space.Record a) => Serializer.Deserialize(Space_Class_FunA(Serializer.Serialize(a, SerializerContext.Space_Record)), SerializerContext.Space_Record);"); + Contains("public static global::Space.Record Proxy_Space_Class_FunA (global::Space.Record a) => Serializer.Deserialize(Space_Class_FunA(Serializer.Serialize(a, SerializerContext.Space_Record)), SerializerContext.Space_Record);"); Contains("""JSImport("Space.Class.funBSerialized", "Bootsharp")] [return: JSMarshalAs>] internal static partial global::System.Threading.Tasks.Task Space_Class_FunB ([JSMarshalAs] global::System.Int64 a);"""); - Contains("public static async global::System.Threading.Tasks.Task Proxy_Space_Class_FunB(global::Space.Record?[]? a) => Serializer.Deserialize(await Space_Class_FunB(Serializer.Serialize(a, SerializerContext.Space_RecordArray)), SerializerContext.Space_RecordArray);"); + Contains("public static async global::System.Threading.Tasks.Task Proxy_Space_Class_FunB (global::Space.Record?[]? a) => Serializer.Deserialize(await Space_Class_FunB(Serializer.Serialize(a, SerializerContext.Space_RecordArray)), SerializerContext.Space_RecordArray);"); } [Fact] @@ -269,4 +269,80 @@ [JSInvokable] public static void Inv () {} Contains("""JSImport("Foo.Imported.funSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_Space_JSImported_Fun ();"""); Contains("""JSImport("Foo.Imported.onEvtSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_Space_JSImported_OnEvt ();"""); } + + [Fact] + public void GeneratesForInterfaceProperties () + { + AddAssembly(With( + """ + [assembly:JSExport(typeof(IExportedStatic))] + [assembly:JSImport(typeof(IImportedStatic))] + + public record Record; + + public interface IExportedStatic + { + Record State { get; set; } + IExportedInstanced Exported { get; } + IImportedInstanced Imported { set; } + int Count { set; } + int Ignored { get => 0; } + int IgnoredToo { set { } } + int this[int index] { get; set; } + } + + public interface IImportedStatic + { + Record State { get; set; } + IImportedInstanced Imported { get; } + IExportedInstanced Exported { set; } + int Count { set; } + } + + public interface IExportedInstanced + { + Record State { get; set; } + IExportedInstanced Exported { get; } + IImportedInstanced Imported { set; } + } + + public interface IImportedInstanced + { + Record State { get; set; } + IImportedInstanced Imported { get; } + IExportedInstanced Exported { set; } + } + + public class Class + { + [JSInvokable] public static IExportedInstanced GetExported (IImportedInstanced arg) => default; + [JSFunction] public static IImportedInstanced GetImported (IExportedInstanced arg) => default; + } + """)); + Execute(); + Contains("JSExport] [return: JSMarshalAs] internal static global::System.Int64 Bootsharp_Generated_Exports_JSExportedStatic_GetPropertyState () => Serializer.Serialize(global::Bootsharp.Generated.Exports.JSExportedStatic.GetPropertyState(), SerializerContext.Record);"); + Contains("JSExport] internal static void Bootsharp_Generated_Exports_JSExportedStatic_SetPropertyState ([JSMarshalAs] global::System.Int64 value) => global::Bootsharp.Generated.Exports.JSExportedStatic.SetPropertyState(Serializer.Deserialize(value, SerializerContext.Record));"); + Contains("""JSImport("ImportedStatic.getPropertyStateSerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Bootsharp_Generated_Imports_JSImportedStatic_GetPropertyState ();"""); + Contains("public static global::Record Proxy_Bootsharp_Generated_Imports_JSImportedStatic_GetPropertyState() => Serializer.Deserialize(Bootsharp_Generated_Imports_JSImportedStatic_GetPropertyState(), SerializerContext.Record);"); + Contains("""JSImport("ImportedStatic.setPropertyStateSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyState ([JSMarshalAs] global::System.Int64 value);"""); + Contains("public static void Proxy_Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyState(global::Record value) => Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyState(Serializer.Serialize(value, SerializerContext.Record));"); + Contains("JSExport] internal static global::System.Int32 Bootsharp_Generated_Exports_JSExportedStatic_GetPropertyExported () => Instances.Register(global::Bootsharp.Generated.Exports.JSExportedStatic.GetPropertyExported());"); + Contains("JSExport] internal static void Bootsharp_Generated_Exports_JSExportedStatic_SetPropertyImported (global::System.Int32 value) => global::Bootsharp.Generated.Exports.JSExportedStatic.SetPropertyImported(new global::Bootsharp.Generated.Imports.JSImportedInstanced(value));"); + Contains("JSExport] internal static void Bootsharp_Generated_Exports_JSExportedStatic_SetPropertyCount (global::System.Int32 value) => global::Bootsharp.Generated.Exports.JSExportedStatic.SetPropertyCount(value);"); + Contains("""JSImport("ImportedStatic.getPropertyImportedSerialized", "Bootsharp")] internal static partial global::System.Int32 Bootsharp_Generated_Imports_JSImportedStatic_GetPropertyImported ();"""); + Contains("public static global::IImportedInstanced Proxy_Bootsharp_Generated_Imports_JSImportedStatic_GetPropertyImported() => (global::IImportedInstanced)new global::Bootsharp.Generated.Imports.JSImportedInstanced(Bootsharp_Generated_Imports_JSImportedStatic_GetPropertyImported());"); + Contains("""JSImport("ImportedStatic.setPropertyExportedSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyExported (global::System.Int32 value);"""); + Contains("public static void Proxy_Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyExported(global::IExportedInstanced value) => Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyExported(Instances.Register(value));"); + Contains("""JSImport("ImportedStatic.setPropertyCountSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyCount (global::System.Int32 value);"""); + Contains("public static void Proxy_Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyCount(global::System.Int32 value) => Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyCount(value);"); + Contains("JSExport] [return: JSMarshalAs] internal static global::System.Int64 Bootsharp_Generated_Exports_JSExportedInstanced_GetPropertyState (global::System.Int32 _id) => Serializer.Serialize(((global::IExportedInstanced)Instances.Get(_id)).State, SerializerContext.Record);"); + Contains("JSExport] internal static void Bootsharp_Generated_Exports_JSExportedInstanced_SetPropertyState (global::System.Int32 _id, [JSMarshalAs] global::System.Int64 value) => ((global::IExportedInstanced)Instances.Get(_id)).State = Serializer.Deserialize(value, SerializerContext.Record);"); + Contains("""JSImport("ImportedInstanced.getPropertyStateSerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Bootsharp_Generated_Imports_JSImportedInstanced_GetPropertyState (global::System.Int32 _id);"""); + Contains("JSExport] internal static global::System.Int32 Bootsharp_Generated_Exports_JSExportedInstanced_GetPropertyExported (global::System.Int32 _id) => Instances.Register(((global::IExportedInstanced)Instances.Get(_id)).Exported);"); + Contains("JSExport] internal static void Bootsharp_Generated_Exports_JSExportedInstanced_SetPropertyImported (global::System.Int32 _id, global::System.Int32 value) => ((global::IExportedInstanced)Instances.Get(_id)).Imported = new global::Bootsharp.Generated.Imports.JSImportedInstanced(value);"); + Contains("""JSImport("ImportedInstanced.getPropertyImportedSerialized", "Bootsharp")] internal static partial global::System.Int32 Bootsharp_Generated_Imports_JSImportedInstanced_GetPropertyImported (global::System.Int32 _id);"""); + Contains("public static global::IImportedInstanced Proxy_Bootsharp_Generated_Imports_JSImportedInstanced_GetPropertyImported(global::System.Int32 _id) => (global::IImportedInstanced)new global::Bootsharp.Generated.Imports.JSImportedInstanced(Bootsharp_Generated_Imports_JSImportedInstanced_GetPropertyImported(_id));"); + Contains("""JSImport("ImportedInstanced.setPropertyExportedSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_JSImportedInstanced_SetPropertyExported (global::System.Int32 _id, global::System.Int32 value);"""); + Contains("public static void Proxy_Bootsharp_Generated_Imports_JSImportedInstanced_SetPropertyExported(global::System.Int32 _id, global::IExportedInstanced value) => Bootsharp_Generated_Imports_JSImportedInstanced_SetPropertyExported(_id, Instances.Register(value));"); + } } diff --git a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs index e4a76dd6..e9e2929e 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs @@ -382,14 +382,32 @@ public class Class } [Fact] - public void IgnoresWriteOnlyAndComputedProperties () + public void SerializersComputedProperties () { AddAssembly(With( """ - public class Node + public record Node + { + public string Id { get; set; } + public string Computed => Id + "something"; + } + + public class Class + { + [JSInvokable] public static Node Echo (Node node) => node; + } + """)); + Execute(); + Contains("System_String.Write(ref writer, value.Computed);"); + } + + [Fact] + public void IgnoresWriteOnlyProperties () + { + AddAssembly(With( + """ + public record Node { - public string Id { get; set; } = string.Empty; - public string Computed => string.Empty; public string WriteOnly { set { } } } @@ -399,7 +417,6 @@ public class Class } """)); Execute(); - DoesNotContain("Computed"); DoesNotContain("WriteOnly"); } diff --git a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs index 9a9cedfc..87f48e37 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs @@ -28,16 +28,23 @@ public void InteropFunctionsImported () public void WhenDebugEnabledEmitsAndUsesExportImportHelpers () { Task.Debug = true; - AddAssembly(WithClass( - """ - [JSInvokable] public static Task InvAsync () => Task.FromResult(0); - [JSFunction] public static void Fun () {} + AddAssembly(With( """ - )); + [assembly:JSExport(typeof(IExportedStatic))] + + public interface IExportedStatic { int State { get; set; } } + + public class Class + { + [JSInvokable] public static Task InvAsync () => Task.FromResult(0); + [JSFunction] public static void Fun () {} + } + """)); Execute(); Contains("function getExport"); Contains("function getImport"); Contains("""getExport("Class_InvAsync")"""); + Contains("""getExport("Bootsharp_Generated_Exports_JSExportedStatic_GetPropertyState")"""); Contains("""getImport(this.funHandler, this.funSerializedHandler, "Class.fun")"""); } @@ -45,16 +52,23 @@ [JSFunction] public static void Fun () {} public void WhenDebugDisabledDoesntEmitAndDoesntUseExportImportHelpers () { Task.Debug = false; - AddAssembly(WithClass( - """ - [JSInvokable] public static Task InvAsync () => Task.FromResult(0); - [JSFunction] public static void Fun () {} + AddAssembly(With( """ - )); + [assembly:JSExport(typeof(IExportedStatic))] + + public interface IExportedStatic { int State { get; set; } } + + public class Class + { + [JSInvokable] public static Task InvAsync () => Task.FromResult(0); + [JSFunction] public static void Fun () {} + } + """)); Execute(); DoesNotContain("function getExport"); DoesNotContain("function getImport"); DoesNotContain("""getExport("Class_InvAsync")"""); + DoesNotContain("""getExport("Bootsharp_Generated_Exports_JSExportedStatic_GetPropertyState")"""); DoesNotContain("""getImport(this.funHandler, this.funSerializedHandler, "Class.fun")"""); } @@ -626,6 +640,103 @@ public interface IImported { void Fun (string s, Enum e); void NotifyEvt (string """); } + [Fact] + public void GeneratesPropertiesForInteropInterfaces () + { + AddAssembly(With( + """ + [assembly:JSExport(typeof(IExportedStatic))] + [assembly:JSImport(typeof(IImportedStatic))] + + public record Info (string Value); + + public interface IExportedStatic + { + Info State { get; set; } + IExportedInstanced Exported { get; } + IImportedInstanced Imported { set; } + int Count { set; } + } + + public interface IImportedStatic + { + Info State { get; set; } + IImportedInstanced Imported { get; } + IExportedInstanced Exported { set; } + int Count { set; } + } + + public interface IExportedInstanced + { + Info State { get; set; } + IExportedInstanced Exported { get; } + IImportedInstanced Imported { set; } + } + + public interface IImportedInstanced + { + Info State { get; set; } + IImportedInstanced Imported { get; } + IExportedInstanced Exported { set; } + } + + public class Class + { + [JSInvokable] public static IExportedInstanced GetExported (IImportedInstanced inst) => default; + [JSFunction] public static IImportedInstanced GetImported (IExportedInstanced inst) => default; + } + """)); + Execute(); + Contains( + """ + class JSExportedInstanced { + constructor(_id) { this._id = _id; disposeOnFinalize(this, _id); } + get state() { return ExportedInstanced.getPropertyState(this._id); } + set state(value) { ExportedInstanced.setPropertyState(this._id, value); } + get exported() { return ExportedInstanced.getPropertyExported(this._id); } + set imported(value) { ExportedInstanced.setPropertyImported(this._id, value); } + } + + export const Class = { + getExported: (inst) => new JSExportedInstanced(exports.Class_GetExported(registerInstance(inst))), + get getImported() { return this.getImportedHandler; }, + set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = (inst) => registerInstance(this.getImportedHandler(new JSExportedInstanced(inst))); }, + get getImportedSerialized() { return this.getImportedSerializedHandler; } + }; + export const ExportedInstanced = { + getPropertyState(_id) { return deserialize(exports.Bootsharp_Generated_Exports_JSExportedInstanced_GetPropertyState(_id), Info); }, + setPropertyState(_id, value) { exports.Bootsharp_Generated_Exports_JSExportedInstanced_SetPropertyState(_id, serialize(value, Info)); }, + getPropertyExported(_id) { return new JSExportedInstanced(exports.Bootsharp_Generated_Exports_JSExportedInstanced_GetPropertyExported(_id)); }, + setPropertyImported(_id, value) { exports.Bootsharp_Generated_Exports_JSExportedInstanced_SetPropertyImported(_id, registerInstance(value)); } + }; + export const ExportedStatic = { + get state() { return deserialize(exports.Bootsharp_Generated_Exports_JSExportedStatic_GetPropertyState(), Info); }, + set state(value) { exports.Bootsharp_Generated_Exports_JSExportedStatic_SetPropertyState(serialize(value, Info)); }, + get exported() { return new JSExportedInstanced(exports.Bootsharp_Generated_Exports_JSExportedStatic_GetPropertyExported()); }, + set imported(value) { exports.Bootsharp_Generated_Exports_JSExportedStatic_SetPropertyImported(registerInstance(value)); }, + set count(value) { exports.Bootsharp_Generated_Exports_JSExportedStatic_SetPropertyCount(value); } + }; + export const ImportedInstanced = { + getPropertyStateSerialized(_id) { return serialize(getInstance(_id).state, Info); }, + setPropertyStateSerialized(_id, value) { getInstance(_id).state = deserialize(value, Info); }, + getPropertyImportedSerialized(_id) { return registerInstance(getInstance(_id).imported); }, + setPropertyExportedSerialized(_id, value) { getInstance(_id).exported = new JSExportedInstanced(value); } + }; + export const ImportedStatic = { + get state() { return this._state; }, + getPropertyStateSerialized() { return serialize(this.state, Info); }, + set state(value) { this._state = value; }, + setPropertyStateSerialized(value) { this.state = deserialize(value, Info); }, + get imported() { return this._imported; }, + getPropertyImportedSerialized() { return registerInstance(this.imported); }, + set exported(value) { this._exported = value; }, + setPropertyExportedSerialized(value) { this.exported = new JSExportedInstanced(value); }, + set count(value) { this._count = value; }, + setPropertyCountSerialized(value) { this.count = value; } + }; + """); + } + [Fact] public void GeneratesForInstancedInteropInterfaces () { diff --git a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs index 13c72e44..c0847d5b 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs @@ -693,29 +693,110 @@ export namespace Class { export interface Foo { } } - - export namespace Class { - export function bar(): Class.Foo; - } """); } [Fact] - public void ExpressionPropertiesAreNotIncluded () + public void ComputedPropertiesAreIncluded () { - AddAssembly( - WithClass("public class Foo { public bool Boo => true; }"), - WithClass("[JSInvokable] public static Foo Bar () => default;")); + AddAssembly(WithClass( + """ + public record Foo + { + public bool Boo => true; + public bool SetOnly { set { } } + public bool this[int index] => true; + } + + [JSInvokable] public static Foo Bar () => default; + """)); Execute(); Contains( """ export namespace Class { export interface Foo { + boo: boolean; } } + """); + } + + [Fact] + public void PropertyDeclarationsAreGeneratedForInteropInterfaces () + { + AddAssembly(With( + """ + [assembly:JSExport(typeof(IExportedStatic))] + [assembly:JSImport(typeof(IImportedStatic))] + + public record Record (string Value); + + public interface IExportedStatic + { + Record State { get; set; } + Record? Optional { get; } + IExportedInstanced Exported { get; } + IImportedInstanced Imported { set; } + } + + public interface IImportedStatic + { + Record State { get; } + IImportedInstanced Imported { get; } + IExportedInstanced Exported { set; } + } + + public interface IExportedInstanced + { + Record State { get; } + IExportedInstanced Exported { get; } + IImportedInstanced Imported { set; } + } + + public interface IImportedInstanced + { + Record State { get; set; } + IImportedInstanced Imported { get; } + IExportedInstanced Exported { set; } + } + + public class Class + { + [JSInvokable] public static IExportedInstanced GetExported () => default; + [JSFunction] public static IImportedInstanced GetImported () => default; + } + """)); + Execute(); + Contains( + """ + export interface IExportedInstanced { + readonly state: Record; + readonly exported: IExportedInstanced; + imported: IImportedInstanced; + } + export interface Record { + value: string; + } + export interface IImportedInstanced { + state: Record; + readonly imported: IImportedInstanced; + exported: IExportedInstanced; + } export namespace Class { - export function bar(): Class.Foo; + export function getExported(): IExportedInstanced; + export let getImported: () => IImportedInstanced; + } + export namespace ExportedStatic { + export let state: Record; + export const optional: Record | null; + export const exported: IExportedInstanced; + export let imported: IImportedInstanced; + } + export namespace ImportedStatic { + export const state: Record; + export const imported: IImportedInstanced; + export let exported: IExportedInstanced; } """); } diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index 7ac595d4..cf87c7ba 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -56,14 +56,14 @@ static bool IsDictionary (Type type) => type.GetGenericTypeDefinition().FullName == typeof(IReadOnlyDictionary<,>).FullName); } - public static NullabilityInfo GetNullability (PropertyInfo property) + public static NullabilityInfo GetNullability (PropertyInfo prop) { - return new NullabilityInfoContext().Create(property); + return new NullabilityInfoContext().Create(prop); } - public static NullabilityInfo GetNullability (ParameterInfo parameter) + public static NullabilityInfo GetNullability (ParameterInfo param) { - return new NullabilityInfoContext().Create(parameter); + return new NullabilityInfoContext().Create(param); } public static bool IsNullable (Type type) => IsNullable(type, out _); @@ -78,10 +78,10 @@ public static bool IsNullable (Type type, NullabilityInfo? info, [NotNullWhen(tr return value != null; } - public static bool IsAutoProperty (PropertyInfo property) + public static bool IsAutoProperty (PropertyInfo prop) { - var backingFieldName = $"<{property.Name}>k__BackingField"; - var backingField = property.DeclaringType!.GetField(backingFieldName, + var backingFieldName = $"<{prop.Name}>k__BackingField"; + var backingField = prop.DeclaringType!.GetField(backingFieldName, BindingFlags.NonPublic | BindingFlags.Instance); return backingField != null; } @@ -116,10 +116,10 @@ public static string BuildJSSpaceFullName (Type type, Preferences prefs) return string.IsNullOrEmpty(space) ? name : $"{space}.{name}"; } - public static (string space, string name, string full) BuildInteropInterfaceImplementationName - (Type instanceType, InterfaceKind kind) + public static (string space, string name, string full) BuildInterfaceImplName + (Type instanceType, InteropKind interop) { - var space = "Bootsharp.Generated." + (kind == InterfaceKind.Export ? "Exports" : "Imports"); + var space = "Bootsharp.Generated." + (interop == InteropKind.Export ? "Exports" : "Imports"); if (instanceType.Namespace != null) space += $".{instanceType.Namespace}"; var name = "JS" + instanceType.Name[1..]; return (space, name, $"{space}.{name}"); diff --git a/src/cs/Bootsharp.Publish/Common/Meta/InterfaceMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/InterfaceMeta.cs index 05dc2871..250e0c3c 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/InterfaceMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/InterfaceMeta.cs @@ -11,7 +11,7 @@ internal sealed record InterfaceMeta /// Whether the interface represents C# API consumed in /// JavaScript (export) or vice versa (import). /// - public required InterfaceKind Kind { get; init; } + public required InteropKind Interop { get; init; } /// /// C# type of the interface. /// @@ -33,22 +33,8 @@ internal sealed record InterfaceMeta /// public string FullName => $"{Namespace}.{Name}"; /// - /// Methods declared on the interface, representing the interop API. + /// Members declared on the interface, representing the interop API. /// - public required IReadOnlyCollection Methods { get; init; } + public required IReadOnlyCollection Members { get; init; } } -/// -/// The type of API interop interface represents. -/// -internal enum InterfaceKind -{ - /// - /// The interface represents C# API consumed in JavaScript. - /// - Export, - /// - /// The interface represents JavaScript API consumed in C#. - /// - Import -} diff --git a/src/cs/Bootsharp.Publish/Common/Meta/InteropKind.cs b/src/cs/Bootsharp.Publish/Common/Meta/InteropKind.cs new file mode 100644 index 00000000..788347e1 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Meta/InteropKind.cs @@ -0,0 +1,16 @@ +namespace Bootsharp.Publish; + +/// +/// Direction of the interop boundary for a discovered API surface. +/// +internal enum InteropKind +{ + /// + /// Implemented in C# and consumed from JavaScript. + /// + Export, + /// + /// Implemented in JavaScript and consumed from C#. + /// + Import +} diff --git a/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs new file mode 100644 index 00000000..29bf6d16 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs @@ -0,0 +1,93 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Bootsharp.Publish; + +/// +/// An interop member declared on a static API surface or interop interface. +/// +internal abstract record MemberMeta +{ + /// + /// Whether the member is implemented in C# and exposed to JavaScript (export) + /// or implemented in JavaScript and consumed from C# (import). + /// + public required InteropKind Interop { get; init; } + /// + /// C# assembly name (DLL file name, w/o the extension), under which the member is declared. + /// + public required string Assembly { get; init; } + /// + /// Full name of the C# type (including namespace), under which the member is declared. + /// + public required string Space { get; init; } + /// + /// JavaScript object name(s) (joined with dot when nested) under which the associated interop + /// member will be declared; resolved from with user-defined converters. + /// + public required string JSSpace { get; init; } + /// + /// C# name of the member, as specified in source code or generated for the interop implementation. + /// + public required string Name { get; init; } + /// + /// JavaScript name of the member as will be specified in source code. + /// + public required string JSName { get; init; } + /// + /// Metadata of the value carried by the member. + /// + public required ValueMeta Value { get; init; } +} + +/// +/// An interop method declared on a static API surface or interop interface. +/// +/// +/// Return value of the method is described in . +/// +internal record MethodMeta : MemberMeta +{ + /// + /// Arguments of the method. + /// + public required IReadOnlyList Arguments { get; init; } + /// + /// Whether the method returns void. + /// + public required bool Void { get; init; } + /// + /// Whether the method returns is task-like value (can be awaited). + /// + public required bool Async { get; init; } +} + +/// +/// An interop event declared on a static API surface or interop interface (temporarily shares the method path). +/// +internal sealed record EventMeta : MethodMeta +{ + /// + /// C# interface method name. It may differ from because event methods + /// on interfaces have special names, which are then renamed on the JS side. This will be removed once + /// we add proper support for events. + /// + public string MethodName { get; } + + [SetsRequiredMembers] // TODO: Remove after implementing proper support for events. + public EventMeta (MethodMeta method, string methodName) : base(method) => MethodName = methodName; +} + +/// +/// An interop property declared on an interop interface. +/// +internal sealed record PropertyMeta : MemberMeta +{ + /// + /// Whether the property has an accessible getter. + /// + public required bool CanGet { get; init; } + /// + /// Whether the property has an accessible setter. + /// + public required bool CanSet { get; init; } +} diff --git a/src/cs/Bootsharp.Publish/Common/Meta/MethodMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/MethodMeta.cs deleted file mode 100644 index c20569f1..00000000 --- a/src/cs/Bootsharp.Publish/Common/Meta/MethodMeta.cs +++ /dev/null @@ -1,78 +0,0 @@ -namespace Bootsharp.Publish; - -/// -/// Interop method. -/// -internal sealed record MethodMeta -{ - /// - /// Type of interop the method is implementing. - /// - public required MethodKind Kind { get; init; } - /// - /// C# assembly name (DLL file name, w/o the extension), under which the method is declared. - /// - public required string Assembly { get; init; } - /// - /// Full name of the C# type (including namespace), under which the method is declared. - /// - public required string Space { get; init; } - /// - /// JavaScript object name(s) (joined with dot when nested) under which the associated interop - /// function will be declared; resolved from with user-defined converters. - /// - public required string JSSpace { get; init; } - /// - /// C# name of the method, as specified in source code. - /// - public required string Name { get; init; } - /// - /// JavaScript name of the method (function), as will be specified in source code. - /// - public required string JSName { get; init; } - /// - /// When the method's class is a generated implementation of an interop interface, contains - /// name of the associated interface method. The name may differ from , - /// which would be the name of the method on the generated interface implementation and is - /// subject to and . - /// - public string? InterfaceName { get; init; } - /// - /// Arguments of the method, in declaration order. - /// - public required IReadOnlyList Arguments { get; init; } - /// - /// Metadata of the value returned by the method. - /// - public required ValueMeta ReturnValue { get; init; } - /// - /// Whether the is void. - /// - public required bool Void { get; init; } - /// - /// Whether the is task-like (can be awaited). - /// - public required bool Async { get; init; } -} - -/// -/// Type of interop method. -/// -internal enum MethodKind -{ - /// - /// The method is implemented in C# and invoked from JavaScript; - /// implementation has . - /// - Invokable, - /// - /// The method is implemented in JavaScript and invoked from C#; - /// implementation has . - /// - Function, - /// - /// The method is invoked from C# to notify subscribers in JavaScript; - /// implementation has . - /// - Event -} diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs index 13a0dc32..98b9226e 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs @@ -10,25 +10,25 @@ public void Report (SolutionInspection inspection) logger.LogMessage(MessageImportance.Normal, "Bootsharp assembly inspection result:"); logger.LogMessage(MessageImportance.Normal, JoinLines("Discovered assemblies:", JoinLines(GetDiscoveredAssemblies(inspection)))); - logger.LogMessage(MessageImportance.Normal, JoinLines("Discovered interop methods:", - JoinLines(GetDiscoveredMethods(inspection)))); + logger.LogMessage(MessageImportance.Normal, JoinLines("Discovered interop members:", + JoinLines(GetDiscoveredMembers(inspection)))); foreach (var warning in inspection.Warnings) logger.LogWarning(warning); } private HashSet GetDiscoveredAssemblies (SolutionInspection inspection) { - return inspection.StaticMethods - .Concat(inspection.StaticInterfaces.SelectMany(i => i.Methods)) - .Select(m => m.Assembly) + return inspection.StaticMethods.Select(m => m.Assembly) + .Concat(inspection.StaticInterfaces.SelectMany(i => i.Members.Select(m => m.Assembly))) + .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Members.Select(m => m.Assembly))) .ToHashSet(); } - private HashSet GetDiscoveredMethods (SolutionInspection inspection) + private HashSet GetDiscoveredMembers (SolutionInspection inspection) { return inspection.StaticMethods - .Concat(inspection.StaticInterfaces.SelectMany(i => i.Methods)) - .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Methods)) + .Concat(inspection.StaticInterfaces.SelectMany(i => i.Members)) + .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Members)) .Select(m => m.ToString()) .ToHashSet(); } diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InterfaceInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InterfaceInspector.cs index d1f33670..4927e73a 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InterfaceInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InterfaceInspector.cs @@ -2,40 +2,58 @@ namespace Bootsharp.Publish; -internal sealed class InterfaceInspector (Preferences prefs, MethodInspector methods, string entryAssemblyName) +internal sealed class InterfaceInspector (Preferences prefs, MemberInspector members, string entryAssemblyName) { - public InterfaceMeta Inspect (Type interfaceType, InterfaceKind kind) + private InteropKind interop; + private (string space, string name, string full) impl; + + public InterfaceMeta Inspect (Type interfaceType, InteropKind interopKind) { - var impl = BuildInteropInterfaceImplementationName(interfaceType, kind); + interop = interopKind; + impl = BuildInterfaceImplName(interfaceType, interop); return new InterfaceMeta { - Kind = kind, + Interop = interop, Type = interfaceType, TypeSyntax = BuildSyntax(interfaceType), Namespace = impl.space, Name = impl.name, - Methods = interfaceType.GetMethods() - .Where(m => m.IsAbstract) - .Select(m => CreateMethod(m, kind, impl.full)).ToArray() + Members = interfaceType.GetProperties().Where(ShouldInspectProperty).Select(CreateProperty) + .Concat(interfaceType.GetMethods().Where(ShouldInspectMethod).Select(CreateMethod)).ToArray() }; } - private MethodMeta CreateMethod (MethodInfo info, InterfaceKind kind, string space) + private bool ShouldInspectMethod (MethodInfo method) + { + return method.IsAbstract && !method.IsSpecialName; + } + + private bool ShouldInspectProperty (PropertyInfo prop) + { + if (prop.GetIndexParameters().Length != 0) return false; + return prop.GetMethod?.IsAbstract == true || prop.SetMethod?.IsAbstract == true; + } + + private MemberMeta CreateMethod (MethodInfo info) { var name = WithPrefs(prefs.Event, info.Name, info.Name); - return methods.Inspect(info, ResolveMethodKind(kind, info, name)) with { + var method = members.Inspect(info, interop) with { Assembly = entryAssemblyName, - Space = space, + Space = impl.full, Name = name, - JSName = ToFirstLower(name), - InterfaceName = info.Name + JSName = ToFirstLower(name) }; + if (interop == InteropKind.Import && name != info.Name) + return new EventMeta(method, info.Name); + return method; } - private MethodKind ResolveMethodKind (InterfaceKind iKind, MethodInfo info, string implMethodName) + private MemberMeta CreateProperty (PropertyInfo info) { - if (iKind == InterfaceKind.Export) return MethodKind.Invokable; - // TODO: This assumes event methods are always renamed via prefs, which may not be the case. - if (implMethodName != info.Name) return MethodKind.Event; - return MethodKind.Function; + return members.Inspect(info, interop) with { + Assembly = entryAssemblyName, + Space = impl.full, + CanGet = info.GetMethod?.IsAbstract == true, + CanSet = info.SetMethod?.IsAbstract == true + }; } } diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs new file mode 100644 index 00000000..4f94ffd2 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs @@ -0,0 +1,59 @@ +using System.Reflection; + +namespace Bootsharp.Publish; + +internal sealed class MemberInspector (Preferences prefs, TypeInspector types, SerializedInspector serde) +{ + public PropertyMeta Inspect (PropertyInfo prop, InteropKind interop) => new() { + Interop = interop, + Assembly = prop.DeclaringType!.Assembly.GetName().Name!, + Space = prop.DeclaringType.FullName!, + JSSpace = BuildJSSpace(prop.DeclaringType), + Name = prop.Name, + JSName = ToFirstLower(prop.Name), + Value = CreateValue(prop.PropertyType, GetNullability(prop)), + CanGet = prop.GetMethod != null, + CanSet = prop.SetMethod != null + }; + + public MethodMeta Inspect (MethodInfo method, InteropKind interop) => new() { + Interop = interop, + Assembly = method.DeclaringType!.Assembly.GetName().Name!, + Space = method.DeclaringType.FullName!, + Name = method.Name, + Arguments = method.GetParameters().Select(CreateArgument).ToArray(), + JSSpace = BuildJSSpace(method.DeclaringType), + JSName = WithPrefs(prefs.Function, method.Name, ToFirstLower(method.Name)), + Value = CreateValue(method.ReturnParameter.ParameterType, GetNullability(method.ReturnParameter)), + Void = IsVoid(method.ReturnParameter.ParameterType), + Async = IsTaskLike(method.ReturnParameter.ParameterType) + }; + + private ArgumentMeta CreateArgument (ParameterInfo param) => new() { + Name = param.Name!, + JSName = param.Name == "function" ? "fn" : param.Name!, + Value = CreateValue(param.ParameterType, GetNullability(param)) + }; + + private ValueMeta CreateValue (Type type, NullabilityInfo nil) + { + IsInstancedInteropInterface(type, out var instanceType); + return new() { + Type = types.Inspect(type), + TypeSyntax = BuildSyntax(type, nil), + Nullable = IsNullable(type, nil), + Nullability = nil, + Serialized = serde.Inspect(type), + InstanceType = instanceType + }; + } + + private string BuildJSSpace (Type type) + { + var space = type.Namespace ?? ""; + var name = BuildJSSpaceName(type); + if (type.IsInterface) name = name[1..]; + var fullname = string.IsNullOrEmpty(space) ? name : $"{space}.{name}"; + return WithPrefs(prefs.Space, fullname, fullname); + } +} diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MethodInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MethodInspector.cs deleted file mode 100644 index 1e07e551..00000000 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MethodInspector.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Reflection; - -namespace Bootsharp.Publish; - -internal sealed class MethodInspector (Preferences prefs, TypeInspector types, SerializedInspector serde) -{ - private MethodInfo method = null!; - private MethodKind kind; - - public MethodMeta Inspect (MethodInfo method, MethodKind kind) - { - this.method = method; - this.kind = kind; - return CreateMethod(); - } - - private MethodMeta CreateMethod () => new() { - Kind = kind, - Assembly = method.DeclaringType!.Assembly.GetName().Name!, - Space = method.DeclaringType.FullName!, - Name = method.Name, - Arguments = method.GetParameters().Select(CreateArgument).ToArray(), - JSSpace = BuildMethodSpace(), - JSName = WithPrefs(prefs.Function, method.Name, ToFirstLower(method.Name)), - ReturnValue = CreateValue(method.ReturnParameter), - Void = IsVoid(method.ReturnParameter.ParameterType), - Async = IsTaskLike(method.ReturnParameter.ParameterType) - }; - - private ArgumentMeta CreateArgument (ParameterInfo param) => new() { - Name = param.Name!, - JSName = param.Name == "function" ? "fn" : param.Name!, - Value = CreateValue(param) - }; - - private ValueMeta CreateValue (ParameterInfo param) - { - var nullability = GetNullability(param); - IsInstancedInteropInterface(param.ParameterType, out var instanceType); - return new() { - Type = types.Inspect(param), - TypeSyntax = BuildSyntax(param.ParameterType, nullability), - Nullable = IsNullable(param.ParameterType, nullability), - Nullability = nullability, - Serialized = serde.Inspect(param), - InstanceType = instanceType - }; - } - - private string BuildMethodSpace () - { - var space = method.DeclaringType!.Namespace ?? ""; - var name = BuildJSSpaceName(method.DeclaringType); - if (method.DeclaringType.IsInterface) name = name[1..]; - var fullname = string.IsNullOrEmpty(space) ? name : $"{space}.{name}"; - return WithPrefs(prefs.Space, fullname, fullname); - } -} diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs index 3cfa47fd..cd5e883e 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs @@ -8,8 +8,6 @@ namespace Bootsharp.Publish; /// Remember that the serialization is only required for the values that cross the interop boundary /// and whose types are not natively supported by System.Runtime.InteropServices.JavaScript. /// The types that are referenced by these top-level interop types are crawled by this inspector. -/// To make sure the serialization inspection is not performed on just arbitrary and unrelated types, -/// the expects and not just a . /// internal sealed class SerializedInspector { @@ -27,9 +25,8 @@ private record Discard (Type Type) : SerializedMeta(Type); private readonly Dictionary byId = []; private readonly HashSet cycle = []; - public SerializedMeta? Inspect (ParameterInfo info) + public SerializedMeta? Inspect (Type type) { - var type = info.ParameterType; return ShouldSerialize(type) ? Build(type) : null; } @@ -131,7 +128,7 @@ static bool HasMatchingParameters (ConstructorInfo ctor, Type declaringType) private static IEnumerable GetSerializableProperties (Type type) { return type.GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(p => p.GetMethod != null && (IsAutoProperty(p) || type.IsInterface)); + .Where(p => p.GetMethod != null && p.GetIndexParameters().Length == 0); } private static IReadOnlyList OrderByDependencyGraph (IEnumerable types) diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs index bfeca7ed..415eece7 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs @@ -21,8 +21,8 @@ internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable /// /// Interop interfaces found in interop method arguments or return values. /// Such interfaces are considered instanced interop APIs, ie stateful objects with - /// interop methods/functions. Both methods of and - /// can be sources of the instanced interfaces. + /// interop methods and properties. Both members of + /// and can be sources of the instanced interfaces. /// public required IReadOnlyCollection InstancedInterfaces { get; init; } /// diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs index 03d063fc..0002ec88 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs @@ -10,13 +10,13 @@ internal sealed class SolutionInspector private readonly List warnings = []; private readonly TypeInspector typeInspector = new(); private readonly SerializedInspector serdeInspector = new(); - private readonly MethodInspector methodInspector; + private readonly MemberInspector memberInspector; private readonly InterfaceInspector interfaceInspector; public SolutionInspector (Preferences prefs, string entryAssemblyName) { - methodInspector = new(prefs, typeInspector, serdeInspector); - interfaceInspector = new(prefs, methodInspector, entryAssemblyName); + memberInspector = new(prefs, typeInspector, serdeInspector); + interfaceInspector = new(prefs, memberInspector, entryAssemblyName); } /// @@ -44,7 +44,7 @@ private void AddSkippedAssemblyWarning (string assemblyPath, Exception exception { var fileName = Path.GetFileName(assemblyPath); var message = $"Failed to inspect '{fileName}' assembly; " + - $"affected methods won't be available in JavaScript. Error: {exception.Message}"; + $"affected interop members won't be available in JavaScript. Error: {exception.Message}"; warnings.Add(message); } @@ -74,58 +74,66 @@ private void InspectExportedType (Type type) private void InspectAssemblyAttribute (CustomAttributeData attribute) { - var kind = default(InterfaceKind); + var interop = default(InteropKind); var name = attribute.AttributeType.FullName; - if (name == typeof(JSExportAttribute).FullName) kind = InterfaceKind.Export; - else if (name == typeof(JSImportAttribute).FullName) kind = InterfaceKind.Import; + if (name == typeof(JSExportAttribute).FullName) interop = InteropKind.Export; + else if (name == typeof(JSImportAttribute).FullName) interop = InteropKind.Import; else return; foreach (var arg in (IEnumerable)attribute.ConstructorArguments[0].Value!) - InspectStaticInteropInterface((Type)arg.Value!, kind); + InspectStaticInteropInterface((Type)arg.Value!, interop); } private void InspectExportedStaticMethod (MethodInfo info) { - var kind = default(MethodKind?); + var interop = default(InteropKind?); + var @event = false; foreach (var attr in info.CustomAttributes.Select(a => a.AttributeType.FullName)) - if (attr == typeof(JSInvokableAttribute).FullName) kind = MethodKind.Invokable; - else if (attr == typeof(JSFunctionAttribute).FullName) kind = MethodKind.Function; - else if (attr == typeof(JSEventAttribute).FullName) kind = MethodKind.Event; - if (kind.HasValue) InspectStaticInteropMethod(info, kind.Value); + if (attr == typeof(JSInvokableAttribute).FullName) interop = InteropKind.Export; + else if (attr == typeof(JSFunctionAttribute).FullName) interop = InteropKind.Import; + else if (attr == typeof(JSEventAttribute).FullName) + { + interop = InteropKind.Import; + @event = true; + } + if (interop.HasValue) InspectStaticInteropMethod(info, interop.Value, @event); } - private void InspectStaticInteropMethod (MethodInfo info, MethodKind kind) + private void InspectStaticInteropMethod (MethodInfo info, InteropKind interop, bool @event) { - var methodMeta = methodInspector.Inspect(info, kind); - staticMethods.Add(methodMeta); - InspectMethodParameters(methodMeta, kind); + var method = memberInspector.Inspect(info, interop); + if (@event) method = new EventMeta(method, info.Name); + staticMethods.Add(method); + InspectMember(method); } - private void InspectStaticInteropInterface (Type type, InterfaceKind kind) + private void InspectStaticInteropInterface (Type type, InteropKind interop) { - var interfaceMeta = interfaceInspector.Inspect(type, kind); + var interfaceMeta = interfaceInspector.Inspect(type, interop); staticInterfaces.Add(interfaceMeta); - foreach (var method in interfaceMeta.Methods) - InspectMethodParameters(method, kind); + foreach (var member in interfaceMeta.Members) + InspectMember(member); } - private void InspectMethodParameters (MethodMeta meta, MethodKind kind) - { - var iKind = kind == MethodKind.Invokable ? InterfaceKind.Export : InterfaceKind.Import; - InspectMethodParameters(meta, iKind); - } - - private void InspectMethodParameters (MethodMeta meta, InterfaceKind kind) + private void InspectMember (MemberMeta meta) { // When interop instance is an argument of exported method, it's imported (JS) API and vice versa. - var argKind = kind == InterfaceKind.Export ? InterfaceKind.Import : InterfaceKind.Export; - foreach (var arg in meta.Arguments) - InspectMethodParameter(arg.Value.Type.Clr, argKind); - if (!meta.Void) InspectMethodParameter(meta.ReturnValue.Type.Clr, kind); + var interop = meta.Interop == InteropKind.Export ? InteropKind.Import : InteropKind.Export; + if (meta is PropertyMeta prop) + { + if (prop.CanSet) InspectType(prop.Value.Type.Clr, interop); + if (prop.CanGet) InspectType(prop.Value.Type.Clr, prop.Interop); + } + else if (meta is MethodMeta method) + { + foreach (var arg in method.Arguments) + InspectType(arg.Value.Type.Clr, interop); + if (!method.Void) InspectType(method.Value.Type.Clr, method.Interop); + } } - private void InspectMethodParameter (Type paramType, InterfaceKind kind) + private void InspectType (Type type, InteropKind interop) { - if (IsInstancedInteropInterface(paramType, out var instanceType)) - instancedInterfaces.Add(interfaceInspector.Inspect(instanceType, kind)); + if (IsInstancedInteropInterface(type, out var instanceType)) + instancedInterfaces.Add(interfaceInspector.Inspect(instanceType, interop)); } } diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/TypeInspector.cs index d2c6d9ef..17077608 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/TypeInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/TypeInspector.cs @@ -1,14 +1,12 @@ -using System.Reflection; - namespace Bootsharp.Publish; internal sealed class TypeInspector { private readonly Dictionary byType = []; - public TypeMeta Inspect (ParameterInfo info) + public TypeMeta Inspect (Type type) { - return Crawl(info.ParameterType); + return Crawl(type); } public IReadOnlyCollection Collect () diff --git a/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs b/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs index f9656b72..d1bb9b5a 100644 --- a/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs @@ -42,7 +42,7 @@ private void AddGeneratedInteropClasses (SolutionInspection inspection) foreach (var inter in inspection.StaticInterfaces) Add(All, inter.FullName, entryAssembly); foreach (var inter in inspection.InstancedInterfaces) - if (inter.Kind == InterfaceKind.Import) + if (inter.Interop == InteropKind.Import) Add(All, inter.FullName, entryAssembly); } diff --git a/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs index 16e45baa..6bab5e46 100644 --- a/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs @@ -5,25 +5,20 @@ namespace Bootsharp.Publish; /// internal sealed class InterfaceGenerator { - private readonly HashSet classes = []; - private readonly HashSet registrations = []; - private HashSet instanced = []; - public string Generate (SolutionInspection inspection) { - instanced = inspection.InstancedInterfaces.ToHashSet(); - foreach (var inter in inspection.StaticInterfaces) - AddInterface(inter); - foreach (var inter in inspection.InstancedInterfaces) - if (inter.Kind == InterfaceKind.Import) - classes.Add(EmitInstancedImportClass(inter)); + var classes = new HashSet(); + foreach (var i in inspection.StaticInterfaces) + if (i.Interop == InteropKind.Export) classes.Add(EmitExportClass(i)); + else classes.Add(EmitImportClass(i)); + foreach (var i in inspection.InstancedInterfaces) + if (i.Interop == InteropKind.Import) + classes.Add(EmitInstancedImportClass(i)); return $$""" #nullable enable #pragma warning disable - {{JoinLines(classes, 0)}} - namespace Bootsharp.Generated { internal static class InterfaceRegistrations @@ -31,18 +26,24 @@ internal static class InterfaceRegistrations [System.Runtime.CompilerServices.ModuleInitializer] internal static void RegisterInterfaces () { - {{JoinLines(registrations, 3)}} + {{JoinLines(inspection.StaticInterfaces.Select(EmitRegistration), 3)}} } } } + + {{JoinLines(classes, 0, "\n\n")}} """; } - private void AddInterface (InterfaceMeta i) + private string EmitRegistration (InterfaceMeta i) { - if (i.Kind == InterfaceKind.Export) classes.Add(EmitExportClass(i)); - else classes.Add(EmitImportClass(i)); - registrations.Add(EmitRegistration(i)); + var inter = i.Interop == InteropKind.Import + ? $"new ImportInterface(new {i.FullName}())" + : $"new ExportInterface(typeof({i.TypeSyntax}), handler => new {i.FullName}(({i.TypeSyntax})handler))"; + var key = i.Interop == InteropKind.Import + ? $"typeof({i.TypeSyntax})" + : $"typeof({i.FullName})"; + return $"Interfaces.Register({key}, {inter});"; } private string EmitExportClass (InterfaceMeta i) => @@ -58,7 +59,7 @@ public class {{i.Name}} {{i.Name}}.handler = handler; } - {{JoinLines(i.Methods.Select(EmitExportMethod), 2)}} + {{JoinLines(i.Members.Select(EmitExport), 2)}} } } """; @@ -69,9 +70,7 @@ namespace {{i.Namespace}} { public class {{i.Name}} : {{i.TypeSyntax}} { - {{JoinLines(i.Methods.Select(m => EmitImportMethod(i, m)), 2)}} - - {{JoinLines(i.Methods.Select(m => EmitImportMethodImplementation(i, m)), 2)}} + {{JoinLines(i.Members.Select(m => EmitImport(i, m)), 2)}} } } """; @@ -84,42 +83,90 @@ public class {{i.Name}}(global::System.Int32 _id) : {{i.TypeSyntax}} { ~{{i.Name}}() => global::Bootsharp.Generated.Interop.DisposeImportedInstance(_id); - {{JoinLines(i.Methods.Select(m => EmitImportMethod(i, m)), 2)}} - - {{JoinLines(i.Methods.Select(m => EmitImportMethodImplementation(i, m)), 2)}} + {{JoinLines(i.Members.Select(m => EmitInstancedImport(i, m)), 2)}} } } """; - private string EmitRegistration (InterfaceMeta i) => i.Kind == InterfaceKind.Import ? - $"Interfaces.Register(typeof({i.TypeSyntax}), new ImportInterface(new {i.FullName}()));" : - $"Interfaces.Register(typeof({i.FullName}), new ExportInterface(typeof({i.TypeSyntax}), handler => new {i.FullName}(({i.TypeSyntax})handler)));"; + private string EmitExport (MemberMeta member) => member switch { + PropertyMeta prop => EmitPropertyExport(prop), + _ => EmitMethodExport((MethodMeta)member) + }; + + private string EmitImport (InterfaceMeta i, MemberMeta member) => member switch { + PropertyMeta prop => EmitPropertyImport(i, prop), + _ => EmitMethodImport(i, (MethodMeta)member), + }; + + private string EmitInstancedImport (InterfaceMeta i, MemberMeta member) => member switch { + PropertyMeta prop => EmitInstancedPropertyImport(i, prop), + _ => EmitInstancedMethodImport(i, (MethodMeta)member) + }; - private string EmitExportMethod (MethodMeta method) + private string EmitPropertyExport (PropertyMeta prop) + { + var name = prop.Name; + var type = prop.Value.TypeSyntax; + var get = $"[JSInvokable] public static {type} GetProperty{name} () => handler.{name};"; + var set = $"[JSInvokable] public static void SetProperty{name} ({type} value) => handler.{name} = value;"; + return JoinLines(0, prop.CanGet ? get : null, prop.CanSet ? set : null); + } + + private string EmitMethodExport (MethodMeta method) { var sigArgs = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var sig = $"public static {method.ReturnValue.TypeSyntax} {method.Name} ({sigArgs})"; + var sig = $"public static {method.Value.TypeSyntax} {method.Name} ({sigArgs})"; var args = string.Join(", ", method.Arguments.Select(a => a.Name)); return $"[JSInvokable] {sig} => handler.{method.Name}({args});"; } - private string EmitImportMethod (InterfaceMeta i, MethodMeta method) + private string EmitPropertyImport (InterfaceMeta i, PropertyMeta prop) + { + var space = $"global::Bootsharp.Generated.Interop.Proxy_{prop.Space.Replace('.', '_')}"; + return + $$""" + {{prop.Value.TypeSyntax}} {{i.TypeSyntax}}.{{prop.Name}} + { + {{JoinLines( + prop.CanGet ? $"get => {space}_GetProperty{prop.Name}();" : null, + prop.CanSet ? $"set => {space}_SetProperty{prop.Name}(value);" : null + )}} + } + """; + } + + private string EmitMethodImport (InterfaceMeta i, MethodMeta method) { - var attr = method.Kind == MethodKind.Function ? "JSFunction" : "JSEvent"; var sigArgs = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - if (instanced.Contains(i)) sigArgs = PrependInstanceIdArgTypeAndName(sigArgs); - var sig = $"public static {method.ReturnValue.TypeSyntax} {method.Name} ({sigArgs})"; + var contract = method is EventMeta @event ? @event.MethodName : method.Name; var args = string.Join(", ", method.Arguments.Select(a => a.Name)); - if (instanced.Contains(i)) args = PrependInstanceIdArgName(args); var name = $"Proxy_{method.Space.Replace('.', '_')}_{method.Name}"; - return $"[{attr}] {sig} => global::Bootsharp.Generated.Interop.{name}({args});"; + return $"{method.Value.TypeSyntax} {i.TypeSyntax}.{contract} ({sigArgs}) => " + + $"global::Bootsharp.Generated.Interop.{name}({args});"; + } + + private string EmitInstancedPropertyImport (InterfaceMeta i, PropertyMeta prop) + { + var space = $"global::Bootsharp.Generated.Interop.Proxy_{prop.Space.Replace('.', '_')}"; + return + $$""" + {{prop.Value.TypeSyntax}} {{i.TypeSyntax}}.{{prop.Name}} + { + {{JoinLines( + prop.CanGet ? $"get => {space}_GetProperty{prop.Name}(_id);" : null, + prop.CanSet ? $"set => {space}_SetProperty{prop.Name}(_id, value);" : null + )}} + } + """; } - private string EmitImportMethodImplementation (InterfaceMeta i, MethodMeta method) + private string EmitInstancedMethodImport (InterfaceMeta i, MethodMeta method) { var sigArgs = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var args = string.Join(", ", method.Arguments.Select(a => a.Name)); - if (instanced.Contains(i)) args = PrependInstanceIdArgName(args); - return $"{method.ReturnValue.TypeSyntax} {i.TypeSyntax}.{method.InterfaceName} ({sigArgs}) => {method.Name}({args});"; + var contract = method is EventMeta @event ? @event.MethodName : method.Name; + var args = PrependInstanceIdArgName(string.Join(", ", method.Arguments.Select(a => a.Name))); + var name = $"Proxy_{method.Space.Replace('.', '_')}_{method.Name}"; + return $"{method.Value.TypeSyntax} {i.TypeSyntax}.{contract} ({sigArgs}) => " + + $"global::Bootsharp.Generated.Interop.{name}({args});"; } } diff --git a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs index e67524b4..d6efc115 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs @@ -14,12 +14,15 @@ internal sealed class InteropGenerator public string Generate (SolutionInspection inspection) { instanced = inspection.InstancedInterfaces; - var @static = inspection.StaticMethods - .Concat(inspection.StaticInterfaces.SelectMany(i => i.Methods)) - .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Methods)); - foreach (var meta in @static) // @formatter:off - if (meta.Kind == MethodKind.Invokable) AddExportMethod(meta); - else { AddImportMethod(meta); AddImportProxy(meta); } // @formatter:on + foreach (var method in inspection.StaticMethods) + if (method.Interop == InteropKind.Export) AddMethodExport(method); + else AddMethodImport(method); + foreach (var inter in inspection.StaticInterfaces) + foreach (var member in inter.Members) + AddMember(member); + foreach (var inter in inspection.InstancedInterfaces) + foreach (var member in inter.Members) + AddMember(member); return $$""" #nullable enable @@ -42,129 +45,191 @@ public static partial class Interop """; } - private void AddExportMethod (MethodMeta inv) + private void AddMember (MemberMeta member) { - var instanced = TryInstanced(inv, out var instance); - var marshalAs = MarshalAmbiguous(inv.ReturnValue, true); - var wait = ShouldWait(inv); - var attr = $"[System.Runtime.InteropServices.JavaScript.JSExport] {marshalAs}"; - methods.Add($"{attr}internal static {BuildSignature()} => {BuildBody()};"); - - string BuildSignature () + switch (member) { - var args = string.Join(", ", inv.Arguments.Select(BuildSignatureArg)); - if (instanced) args = args = PrependInstanceIdArgTypeAndName(args); - var @return = BuildReturnValue(inv); - var signature = $"{@return} {BuildMethodName(inv)} ({args})"; - if (wait) signature = $"async {signature}"; - return signature; + case PropertyMeta { Interop: InteropKind.Export } p: AddPropertyExport(p); break; + case PropertyMeta { Interop: InteropKind.Import } p: AddPropertyImport(p); break; + case MethodMeta { Interop: InteropKind.Export } m: AddMethodExport(m); break; + case MethodMeta { Interop: InteropKind.Import } m: AddMethodImport(m); break; } + } - string BuildBody () + private void AddPropertyExport (PropertyMeta prop) + { + var instanced = TryInstanced(prop, out var instance); + if (prop.CanGet) { - var args = string.Join(", ", inv.Arguments.Select(BuildBodyArg)); + var marshalAs = MarshalAmbiguous(prop.Value, true); + var attr = $"[System.Runtime.InteropServices.JavaScript.JSExport] {marshalAs}"; + var name = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; + var args = instanced ? PrependInstanceIdArgTypeAndName("") : ""; var body = instanced - ? $"(({instance!.TypeSyntax})Instances.Get(_id)).{inv.Name}({args})" - : $"global::{inv.Space}.{inv.Name}({args})"; - if (wait) body = $"await {body}"; - if (inv.ReturnValue.IsInstance) body = $"Instances.Register({body})"; - else if (Serialized(inv.ReturnValue, out var id)) body = $"Serializer.Serialize({body}, {id})"; - return body; + ? $"(({instance!.TypeSyntax})Instances.Get(_id)).{prop.Name}" + : $"global::{prop.Space}.GetProperty{prop.Name}()"; + if (prop.Value.IsInstance) body = $"Instances.Register({body})"; + else if (Serialized(prop.Value, out var id)) body = $"Serializer.Serialize({body}, {id})"; + methods.Add($"{attr}internal static {BuildValueSyntax(prop.Value)} {name} ({args}) => {body};"); } - - string BuildBodyArg (ArgumentMeta arg) + if (prop.CanSet) { - if (arg.Value.IsInstance) - { - var (_, _, full) = BuildInteropInterfaceImplementationName(arg.Value.InstanceType, InterfaceKind.Import); - return $"new global::{full}({arg.Name})"; - } - if (Serialized(arg.Value, out var id)) return $"Serializer.Deserialize({arg.Name}, {id})"; - return arg.Name; + var attr = "[System.Runtime.InteropServices.JavaScript.JSExport] "; + var name = $"{prop.Space.Replace('.', '_')}_SetProperty{prop.Name}"; + var args = BuildParameter(prop.Value, "value"); + if (instanced) args = PrependInstanceIdArgTypeAndName(args); + var value = prop.Value.InstanceType is { } it + ? $"new global::{BuildInterfaceImplName(it, InteropKind.Import).full}(value)" + : Serialized(prop.Value, out var id) ? $"Serializer.Deserialize(value, {id})" : "value"; + var body = instanced + ? $"(({instance!.TypeSyntax})Instances.Get(_id)).{prop.Name} = {value}" + : $"global::{prop.Space}.SetProperty{prop.Name}({value})"; + methods.Add($"{attr}internal static void {name} ({args}) => {body};"); } } - private void AddImportMethod (MethodMeta method) + private void AddPropertyImport (PropertyMeta prop) { - var args = string.Join(", ", method.Arguments.Select(BuildSignatureArg)); - if (TryInstanced(method, out _)) args = PrependInstanceIdArgTypeAndName(args); - var @return = BuildReturnValue(method); - var endpoint = $"{method.JSSpace}.{method.JSName}Serialized"; - var attr = $"""[System.Runtime.InteropServices.JavaScript.JSImport("{endpoint}", "Bootsharp")]"""; - var marsh = MarshalAmbiguous(method.ReturnValue, true); - methods.Add($"{attr} {marsh}internal static partial {@return} {BuildMethodName(method)} ({args});"); + var instanced = TryInstanced(prop, out _); + if (prop.CanGet) + { + var endpoint = $"""("{prop.JSSpace}.getProperty{prop.Name}Serialized", "Bootsharp")"""; + var marshalAs = MarshalAmbiguous(prop.Value, true); + var attr = $"[System.Runtime.InteropServices.JavaScript.JSImport{endpoint}] {marshalAs}"; + var name = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; + var args = instanced ? PrependInstanceIdArgTypeAndName("") : ""; + methods.Add($"{attr}internal static partial {BuildValueSyntax(prop.Value)} {name} ({args});"); + } + if (prop.CanSet) + { + var endpoint = $"""("{prop.JSSpace}.setProperty{prop.Name}Serialized", "Bootsharp")"""; + var attr = $"[System.Runtime.InteropServices.JavaScript.JSImport{endpoint}] "; + var name = $"{prop.Space.Replace('.', '_')}_SetProperty{prop.Name}"; + var args = BuildParameter(prop.Value, "value"); + if (instanced) args = PrependInstanceIdArgTypeAndName(args); + methods.Add($"{attr}internal static partial void {name} ({args});"); + } + AddPropertyImportProxy(prop); } - private void AddImportProxy (MethodMeta method) + private void AddPropertyImportProxy (PropertyMeta prop) { - var instanced = TryInstanced(method, out _); - var name = $"Proxy_{BuildMethodName(method)}"; - var @return = method.ReturnValue.TypeSyntax; - var args = string.Join(", ", method.Arguments.Select(arg => $"{arg.Value.TypeSyntax} {arg.Name}")); - if (instanced) args = args = PrependInstanceIdArgTypeAndName(args); - var wait = ShouldWait(method); - var async = wait ? "async " : ""; - methods.Add($"public static {async}{@return} {name}({args}) => {BuildBody()};"); - - string BuildBody () + var instanced = TryInstanced(prop, out _); + if (prop.CanGet) + { + var name = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; + var args = instanced ? PrependInstanceIdArgTypeAndName("") : ""; + var body = instanced ? $"{name}(_id)" : $"{name}()"; + if (prop.Value.InstanceType is { } it) + body = $"({BuildSyntax(it)})new global::{BuildInterfaceImplName(it, InteropKind.Import).full}({body})"; + else if (Serialized(prop.Value, out var id)) body = $"Serializer.Deserialize({body}, {id})"; + methods.Add($"public static {prop.Value.TypeSyntax} Proxy_{name}({args}) => {body};"); + } + if (prop.CanSet) { - var args = string.Join(", ", method.Arguments.Select(BuildBodyArg)); - if (instanced) args = PrependInstanceIdArgName(args); - var body = $"{BuildMethodName(method)}({args})"; - if (wait) body = $"await {body}"; - if (method.ReturnValue.InstanceType is { } itp) - { - var (_, _, full) = BuildInteropInterfaceImplementationName(itp, InterfaceKind.Import); - return $"({BuildSyntax(itp)})new global::{full}({body})"; - } - if (Serialized(method.ReturnValue, out var id)) return $"Serializer.Deserialize({body}, {id})"; - return body; + var name = $"{prop.Space.Replace('.', '_')}_SetProperty{prop.Name}"; + var args = $"{prop.Value.TypeSyntax} value"; + if (instanced) args = PrependInstanceIdArgTypeAndName(args); + var value = prop.Value.IsInstance ? "Instances.Register(value)" : + Serialized(prop.Value, out var id) ? $"Serializer.Serialize(value, {id})" : "value"; + var body = instanced ? $"{name}(_id, {value})" : $"{name}({value})"; + methods.Add($"public static void Proxy_{name}({args}) => {body};"); } + } + + private void AddMethodExport (MethodMeta method) + { + var instanced = TryInstanced(method, out var instance); + var wait = ShouldWait(method); + var marshalAs = MarshalAmbiguous(method.Value, true); + var attr = $"[System.Runtime.InteropServices.JavaScript.JSExport] {marshalAs}"; + var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; + var @return = BuildValueSyntax(method.Value); + if (wait) @return = $"async global::System.Threading.Tasks.Task<{@return}>"; + var sigArgs = string.Join(", ", method.Arguments.Select(a => BuildParameter(a.Value, a.Name))); + if (instanced) sigArgs = PrependInstanceIdArgTypeAndName(sigArgs); + var callArgs = string.Join(", ", method.Arguments.Select(BuildCallArg)); + var body = instanced + ? $"(({instance!.TypeSyntax})Instances.Get(_id)).{method.Name}({callArgs})" + : $"global::{method.Space}.{method.Name}({callArgs})"; + if (wait) body = $"await {body}"; + if (method.Value.IsInstance) body = $"Instances.Register({body})"; + else if (Serialized(method.Value, out var id)) body = $"Serializer.Serialize({body}, {id})"; + methods.Add($"{attr}internal static {@return} {name} ({sigArgs}) => {body};"); - string BuildBodyArg (ArgumentMeta arg) + string BuildCallArg (ArgumentMeta arg) { - if (arg.Value.IsInstance) return $"Instances.Register({arg.Name})"; - if (Serialized(arg.Value, out var id)) return $"Serializer.Serialize({arg.Name}, {id})"; + if (arg.Value.InstanceType is { } it) + return $"new global::{BuildInterfaceImplName(it, InteropKind.Import).full}({arg.Name})"; + if (Serialized(arg.Value, out var id)) return $"Serializer.Deserialize({arg.Name}, {id})"; return arg.Name; } } - private string BuildValueType (ValueMeta value) + private void AddMethodImport (MethodMeta method) { - var nil = value.Nullable && !value.IsSerialized ? "?" : ""; - if (value.IsInstance) return $"global::System.Int32{nil}"; - if (value.IsSerialized) return $"global::System.Int64{nil}"; - return value.TypeSyntax; + var instanced = TryInstanced(method, out _); + var marshalAs = MarshalAmbiguous(method.Value, true); + var endpoint = $"""("{method.JSSpace}.{method.JSName}Serialized", "Bootsharp")"""; + var attr = $"[System.Runtime.InteropServices.JavaScript.JSImport{endpoint}] {marshalAs}"; + var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; + var @return = BuildValueSyntax(method.Value); + if (ShouldWait(method)) @return = $"global::System.Threading.Tasks.Task<{@return}>"; + var args = string.Join(", ", method.Arguments.Select(a => BuildParameter(a.Value, a.Name))); + if (instanced) args = PrependInstanceIdArgTypeAndName(args); + methods.Add($"{attr}internal static partial {@return} {name} ({args});"); + AddMethodImportProxy(method); } - private string BuildSignatureArg (ArgumentMeta arg) + private void AddMethodImportProxy (MethodMeta method) { - var type = BuildValueType(arg.Value); - return $"{MarshalAmbiguous(arg.Value, false)}{type} {arg.Name}"; + var instanced = TryInstanced(method, out _); + var wait = ShouldWait(method); + var @return = $"{(wait ? "async " : "")}{method.Value.TypeSyntax}"; + var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; + var sigArgs = string.Join(", ", method.Arguments.Select(arg => $"{arg.Value.TypeSyntax} {arg.Name}")); + if (instanced) sigArgs = PrependInstanceIdArgTypeAndName(sigArgs); + var callArgs = string.Join(", ", method.Arguments.Select(BuildCallArg)); + if (instanced) callArgs = PrependInstanceIdArgName(callArgs); + var body = $"{name}({callArgs})"; + if (wait) body = $"await {body}"; + if (method.Value.InstanceType is { } it) + body = $"({BuildSyntax(it)})new global::{BuildInterfaceImplName(it, InteropKind.Import).full}({body})"; + else if (Serialized(method.Value, out var id)) body = $"Serializer.Deserialize({body}, {id})"; + methods.Add($"public static {@return} Proxy_{name} ({sigArgs}) => {body};"); + + string BuildCallArg (ArgumentMeta arg) + { + if (arg.Value.IsInstance) return $"Instances.Register({arg.Name})"; + if (Serialized(arg.Value, out var id)) return $"Serializer.Serialize({arg.Name}, {id})"; + return arg.Name; + } } - private string BuildReturnValue (MethodMeta method) + private string BuildParameter (ValueMeta value, string name) { - var syntax = BuildValueType(method.ReturnValue); - if (ShouldWait(method)) syntax = $"global::System.Threading.Tasks.Task<{syntax}>"; - return syntax; + var type = BuildValueSyntax(value); + return $"{MarshalAmbiguous(value, false)}{type} {name}"; } - private string BuildMethodName (MethodMeta method) + private string BuildValueSyntax (ValueMeta value) { - return $"{method.Space.Replace('.', '_')}_{method.Name}"; + var nil = value.Nullable && !value.IsSerialized ? "?" : ""; + if (value.IsInstance) return $"global::System.Int32{nil}"; + if (value.IsSerialized) return $"global::System.Int64{nil}"; + return value.TypeSyntax; } - private bool TryInstanced (MethodMeta method, [NotNullWhen(true)] out InterfaceMeta? instance) + private bool TryInstanced (MemberMeta member, [NotNullWhen(true)] out InterfaceMeta? instance) { - instance = instanced.FirstOrDefault(i => i.Methods.Contains(method)); + instance = instanced.FirstOrDefault(i => i.Members.Contains(member)); return instance is not null; } private bool ShouldWait (MethodMeta method) { if (!method.Async) return false; - return method.ReturnValue.IsSerialized || method.ReturnValue.IsInstance; + return method.Value.IsSerialized || method.Value.IsInstance; } private static string MarshalAmbiguous (ValueMeta value, bool @return) diff --git a/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs index a98a0646..5bfceb41 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs @@ -4,7 +4,7 @@ internal sealed class InteropInitializerGenerator { public string Generate (IEnumerable methods) { - var interop = methods.Where(m => m.Kind is MethodKind.Function or MethodKind.Event) + var interop = methods.Where(m => m.Interop == InteropKind.Import) .OrderBy(BuildProxyName).ToArray(); if (interop.Length == 0) return ""; return $$""" @@ -39,7 +39,7 @@ private static string BuildAssignment (MethodMeta method) private static string BuildPointerType (MethodMeta method) { var args = method.Arguments.Select(a => a.Value.TypeSyntax).ToList(); - args.Add(method.ReturnValue.TypeSyntax); + args.Add(method.Value.TypeSyntax); return $"delegate* managed<{string.Join(", ", args)}>"; } diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs index 4d1e4ecd..f502e27e 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs @@ -4,23 +4,34 @@ internal sealed class BindingClassGenerator { public string Generate (IReadOnlyCollection instanced) { - var exported = instanced.Where(i => i.Kind == InterfaceKind.Export); - return JoinLines(exported.Select(BuildClass), 0) + '\n'; + var exported = instanced.Where(i => i.Interop == InteropKind.Export); + return JoinLines(exported.Select(EmitClass), 0) + '\n'; } - private string BuildClass (InterfaceMeta inter) => + private string EmitClass (InterfaceMeta inter) => $$""" class {{BuildJSInteropInstanceClassName(inter)}} { constructor(_id) { this._id = _id; disposeOnFinalize(this, _id); } - {{JoinLines(inter.Methods.Select(BuildFunction))}} + {{JoinLines(inter.Members.Select(EmitMember))}} } """; - private string BuildFunction (MethodMeta inv) + private string EmitMember (MemberMeta member) => member switch { + PropertyMeta prop => EmitProperty(prop), + _ => EmitMethod((MethodMeta)member) + }; + + private string EmitMethod (MethodMeta method) { - var sigArgs = string.Join(", ", inv.Arguments.Select(a => a.Name)); - var args = "this._id" + (sigArgs.Length > 0 ? $", {sigArgs}" : ""); - var @return = inv.Void ? "" : "return "; - return $"{inv.JSName}({sigArgs}) {{ {@return}{inv.JSSpace}.{inv.JSName}({args}); }}"; + var sigArgs = string.Join(", ", method.Arguments.Select(a => a.Name)); + var callArgs = sigArgs.Length > 0 ? $"this._id, {sigArgs}" : "this._id"; + var body = $"{method.JSSpace}.{method.JSName}({callArgs})"; + if (!method.Void) body = $"return {body}"; + return $"{method.JSName}({sigArgs}) {{ {body}; }}"; } + + private string EmitProperty (PropertyMeta p) => JoinLines(0, + p.CanGet ? $"get {p.JSName}() {{ return {p.JSSpace}.getProperty{p.Name}(this._id); }}" : null, + p.CanSet ? $"set {p.JSName}(value) {{ {p.JSSpace}.setProperty{p.Name}(this._id, value); }}" : null + ); } diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs index f52e73e2..41381998 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs @@ -4,7 +4,7 @@ namespace Bootsharp.Publish; internal sealed class BindingGenerator (Preferences prefs, bool debug) { - private record Binding (MethodMeta? Method, Type? Enum, string Namespace); + private record Binding (MemberMeta? Member, Type? Enum, string Namespace); private readonly StringBuilder builder = new(); private readonly BindingClassGenerator classGenerator = new(); @@ -21,12 +21,12 @@ private record Binding (MethodMeta? Method, Type? Enum, string Namespace); public string Generate (SolutionInspection inspection) { instanced = inspection.InstancedInterfaces; - var methods = inspection.StaticMethods - .Concat(inspection.StaticInterfaces.SelectMany(i => i.Methods)) - .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Methods)) - .ToArray(); - bindings = methods + bindings = inspection.StaticMethods .Select(m => new Binding(m, null, m.JSSpace)) + .Concat(inspection.StaticInterfaces.SelectMany(i => i.Members + .Select(m => new Binding(m, null, m.JSSpace)))) + .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Members + .Select(m => new Binding(m, null, m.JSSpace)))) .Concat(inspection.Serialized.Where(t => t.Type.IsEnum) .Select(t => new Binding(null, t.Type, BuildJSSpace(t.Type, prefs)))) .OrderBy(m => m.Namespace).ToArray(); @@ -91,7 +91,7 @@ function getImport(handler, serializedHandler, name) { private void EmitBinding () { if (ShouldOpenNamespace()) OpenNamespace(); - if (binding.Method != null) EmitMethod(binding.Method); + if (binding.Member != null) EmitMember(binding.Member); else EmitEnum(binding.Enum!); if (ShouldCloseNamespace()) CloseNamespace(); } @@ -110,7 +110,7 @@ private void OpenNamespace () while (prevParts.ElementAtOrDefault(level) == parts[level]) level++; for (var i = level; i < parts.Length; level = i, i++) if (i == 0) builder.Append($"\nexport const {parts[i]} = {{"); - else builder.Append($"{Comma()}\n{Pad(i)}{parts[i]}: {{"); + else builder.Append($"{Comma}\n{Pad(i)}{parts[i]}: {{"); } private bool ShouldCloseNamespace () @@ -139,80 +139,80 @@ int GetCloseLevel () } } - private void EmitMethod (MethodMeta method) + private void EmitMember (MemberMeta member) { - if (method.Kind == MethodKind.Invokable) EmitInvokable(method); - else if (method.Kind == MethodKind.Function) EmitFunction(method); - else EmitEvent(method); + switch (member) + { + case EventMeta e: EmitEvent(e); break; + case PropertyMeta { Interop: InteropKind.Export } p: EmitPropertyExport(p); break; + case PropertyMeta { Interop: InteropKind.Import } p: EmitPropertyImport(p); break; + case MethodMeta { Interop: InteropKind.Export } m: EmitMethodExport(m); break; + case MethodMeta { Interop: InteropKind.Import } m: EmitMethodImport(m); break; + } } - private void EmitInvokable (MethodMeta method) + private void EmitPropertyExport (PropertyMeta prop) { - var instanced = IsInstanced(method); - var wait = ShouldWait(method); - var fn = $"{method.Space.Replace('.', '_')}_{method.Name}"; - var endpoint = debug ? $"""getExport("{fn}")""" : $"exports.{fn}"; - var funcArgs = string.Join(", ", method.Arguments.Select(a => a.JSName)); - if (instanced) funcArgs = PrependInstanceIdArgName(funcArgs); - var invArgs = string.Join(", ", method.Arguments.Select(BuildInvArg)); - if (instanced) invArgs = PrependInstanceIdArgName(invArgs); - var body = $"{(wait ? "await " : "")}{endpoint}({invArgs})"; - if (method.ReturnValue.InstanceType is { } itp) body = $"new {BuildInstanceClassName(itp)}({body})"; - else if (method.ReturnValue.IsSerialized) body = $"deserialize({body}, {method.ReturnValue.Serialized.Id})"; - var func = $"{(wait ? "async " : "")}({funcArgs}) => {body}"; - builder.Append($"{Break()}{method.JSName}: {func}"); - - string BuildInvArg (ArgumentMeta arg) + var instanced = this.instanced.Any(i => i.Members.Contains(prop)); + if (prop.CanGet) + { + var fnName = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; + var endpoint = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; + var body = instanced ? $"{endpoint}(_id)" : $"{endpoint}()"; + if (prop.Value.InstanceType is { } it) + body = $"new {BuildJSInteropInstanceClassName(this.instanced.First(i => i.Type == it))}({body})"; + else if (prop.Value.IsSerialized) body = $"deserialize({body}, {prop.Value.Serialized.Id})"; + if (instanced) builder.Append($"{Br}getProperty{prop.Name}(_id) {{ return {body}; }}"); + else builder.Append($"{Br}get {prop.JSName}() {{ return {body}; }}"); + } + if (prop.CanSet) { - if (arg.Value.IsInstance) return $"registerInstance({arg.JSName})"; - if (arg.Value.IsSerialized) return $"serialize({arg.JSName}, {arg.Value.Serialized.Id})"; - return arg.JSName; + var fnName = $"{prop.Space.Replace('.', '_')}_SetProperty{prop.Name}"; + var endpoint = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; + var value = prop.Value.IsInstance ? "registerInstance(value)" : + prop.Value.IsSerialized ? $"serialize(value, {prop.Value.Serialized.Id})" : "value"; + var body = instanced ? $"{endpoint}(_id, {value})" : $"{endpoint}({value})"; + if (instanced) builder.Append($"{Br}setProperty{prop.Name}(_id, value) {{ {body}; }}"); + else builder.Append($"{Br}set {prop.JSName}(value) {{ {body}; }}"); } } - private void EmitFunction (MethodMeta method) + private void EmitPropertyImport (PropertyMeta prop) { - var instanced = IsInstanced(method); - var wait = ShouldWait(method); - var name = method.JSName; - var funcArgs = string.Join(", ", method.Arguments.Select(a => a.JSName)); - if (instanced) funcArgs = PrependInstanceIdArgName(funcArgs); - var invArgs = string.Join(", ", method.Arguments.Select(BuildInvArg)); - var handler = instanced ? $"getInstance(_id).{name}" : $"this.{name}Handler"; - var body = $"{(wait ? "await " : "")}{handler}({invArgs})"; - if (method.ReturnValue.IsInstance) body = $"registerInstance({body})"; - else if (method.ReturnValue.IsSerialized) body = $"serialize({body}, {method.ReturnValue.Serialized.Id})"; - var serdeHandler = $"{(wait ? "async " : "")}({funcArgs}) => {body}"; - if (instanced) builder.Append($"{Break()}{name}Serialized: {serdeHandler}"); - else + var instanced = this.instanced.Any(i => i.Members.Contains(prop)); + if (prop.CanGet) { - var serde = $"this.{name}SerializedHandler"; - var serdeExp = debug ? $"getImport({handler}, {serde}, \"{binding.Namespace}.{name}\")" : serde; - builder.Append($"{Break()}get {name}() {{ return {handler}; }}"); - builder.Append($"{Break()}set {name}(handler) {{ {handler} = handler; {serde} = {serdeHandler}; }}"); - builder.Append($"{Break()}get {name}Serialized() {{ return {serdeExp}; }}"); + if (!instanced) builder.Append($"{Br}get {prop.JSName}() {{ return this._{prop.JSName}; }}"); + var args = instanced ? "_id" : ""; + var body = instanced ? $"getInstance(_id).{prop.JSName}" : $"this.{prop.JSName}"; + if (prop.Value.IsInstance) body = $"registerInstance({body})"; + else if (prop.Value.IsSerialized) body = $"serialize({body}, {prop.Value.Serialized.Id})"; + builder.Append($"{Br}getProperty{prop.Name}Serialized({args}) {{ return {body}; }}"); } - - string BuildInvArg (ArgumentMeta arg) + if (prop.CanSet) { - if (arg.Value.IsInstance) return $"new {BuildInstanceClassName(arg.Value.InstanceType)}({arg.JSName})"; - if (arg.Value.IsSerialized) return $"deserialize({arg.JSName}, {arg.Value.Serialized.Id})"; - return arg.JSName; + if (!instanced) builder.Append($"{Br}set {prop.JSName}(value) {{ this._{prop.JSName} = value; }}"); + var value = prop.Value.InstanceType is { } it + ? $"new {BuildJSInteropInstanceClassName(this.instanced.First(i => i.Type == it))}(value)" + : prop.Value.IsSerialized ? $"deserialize(value, {prop.Value.Serialized.Id})" : "value"; + var args = instanced ? "_id, value" : "value"; + var body = instanced ? $"getInstance(_id).{prop.JSName} = {value}" : $"this.{prop.JSName} = {value}"; + builder.Append($"{Br}setProperty{prop.Name}Serialized({args}) {{ {body}; }}"); } } - private void EmitEvent (MethodMeta method) + private void EmitEvent (EventMeta method) { - var instanced = IsInstanced(method); + var instanced = this.instanced.Any(i => i.Members.Contains(method)); var name = method.JSName; - if (!instanced) builder.Append($"{Break()}{name}: new Event()"); - var funcArgs = string.Join(", ", method.Arguments.Select(a => a.JSName)); - if (instanced) funcArgs = PrependInstanceIdArgName(funcArgs); - var evtArgs = string.Join(", ", method.Arguments.Select(BuildEvtArg)); + if (!instanced) builder.Append($"{Br}{name}: new Event()"); + var sigArgs = string.Join(", ", method.Arguments.Select(a => a.JSName)); + if (instanced) sigArgs = PrependInstanceIdArgName(sigArgs); + var callArgs = string.Join(", ", method.Arguments.Select(BuildCallArg)); var handler = instanced ? "getInstance(_id)" : method.JSSpace; - builder.Append($"{Break()}{name}Serialized: ({funcArgs}) => {handler}.{name}.broadcast({evtArgs})"); + builder.Append($"{Br}{name}Serialized: ({sigArgs}) => {handler}.{name}.broadcast({callArgs})"); - string BuildEvtArg (ArgumentMeta arg) + string BuildCallArg (ArgumentMeta arg) { if (!arg.Value.IsSerialized) return arg.JSName; // By default, we use 'null' for missing collection items, but here the event args array @@ -222,34 +222,81 @@ string BuildEvtArg (ArgumentMeta arg) } } + private void EmitMethodExport (MethodMeta method) + { + var instanced = this.instanced.Any(i => i.Members.Contains(method)); + var wait = ShouldWait(method); + var fnName = $"{method.Space.Replace('.', '_')}_{method.Name}"; + var endpoint = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; + var sigArgs = string.Join(", ", method.Arguments.Select(a => a.JSName)); + if (instanced) sigArgs = PrependInstanceIdArgName(sigArgs); + var callArgs = string.Join(", ", method.Arguments.Select(BuildCallArg)); + if (instanced) callArgs = PrependInstanceIdArgName(callArgs); + var body = $"{(wait ? "await " : "")}{endpoint}({callArgs})"; + if (method.Value.InstanceType is { } it) + body = $"new {BuildJSInteropInstanceClassName(this.instanced.First(i => i.Type == it))}({body})"; + else if (method.Value.IsSerialized) body = $"deserialize({body}, {method.Value.Serialized.Id})"; + builder.Append($"{Br}{method.JSName}: {(wait ? "async " : "")}({sigArgs}) => {body}"); + + string BuildCallArg (ArgumentMeta arg) + { + var name = arg.JSName; + if (arg.Value.IsInstance) name = $"registerInstance({name})"; + else if (arg.Value.IsSerialized) name = $"serialize({name}, {arg.Value.Serialized.Id})"; + return name; + } + } + + private void EmitMethodImport (MethodMeta method) + { + var instanced = this.instanced.Any(i => i.Members.Contains(method)); + var wait = ShouldWait(method); + var fnName = method.JSName; + var sigArgs = string.Join(", ", method.Arguments.Select(a => a.JSName)); + if (instanced) sigArgs = PrependInstanceIdArgName(sigArgs); + var callArgs = string.Join(", ", method.Arguments.Select(BuildCallArg)); + var handler = instanced ? $"getInstance(_id).{fnName}" : $"this.{fnName}Handler"; + var body = $"{(wait ? "await " : "")}{handler}({callArgs})"; + if (method.Value.IsInstance) body = $"registerInstance({body})"; + else if (method.Value.IsSerialized) body = $"serialize({body}, {method.Value.Serialized.Id})"; + var serdeHandler = $"{(wait ? "async " : "")}({sigArgs}) => {body}"; + if (instanced) builder.Append($"{Br}{fnName}Serialized: {serdeHandler}"); + else + { + var serde = $"this.{fnName}SerializedHandler"; + var serdeExp = debug ? $"getImport({handler}, {serde}, \"{binding.Namespace}.{fnName}\")" : serde; + builder.Append($"{Br}get {fnName}() {{ return {handler}; }}"); + builder.Append($"{Br}set {fnName}(handler) {{ {handler} = handler; {serde} = {serdeHandler}; }}"); + builder.Append($"{Br}get {fnName}Serialized() {{ return {serdeExp}; }}"); + } + + string BuildCallArg (ArgumentMeta arg) + { + var name = arg.JSName; + if (arg.Value.InstanceType is { } it) + name = $"new {BuildJSInteropInstanceClassName(this.instanced.First(i => i.Type == it))}({name})"; + else if (arg.Value.IsSerialized) name = $"deserialize({name}, {arg.Value.Serialized.Id})"; + return name; + } + } + private void EmitEnum (Type @enum) { var values = Enum.GetValuesAsUnderlyingType(@enum).Cast().ToArray(); var fields = string.Join(", ", values .Select(v => $"\"{v}\": \"{Enum.GetName(@enum, v)}\"") .Concat(values.Select(v => $"\"{Enum.GetName(@enum, v)}\": {v}"))); - builder.Append($"{Break()}{@enum.Name}: {{ {fields} }}"); + builder.Append($"{Br}{@enum.Name}: {{ {fields} }}"); } private bool ShouldWait (MethodMeta method) { if (!method.Async) return false; return method.Arguments.Any(a => a.Value.IsSerialized || a.Value.IsInstance) || - method.ReturnValue.IsSerialized || method.ReturnValue.IsInstance; + method.Value.IsSerialized || method.Value.IsInstance; } - private string Break () => $"{Comma()}\n{Pad(level + 1)}"; + private string Br => $"{Comma}\n{Pad(level + 1)}"; private string Pad (int level) => new(' ', level * 4); - private string Comma () => builder[^1] == '{' ? "" : ","; - - private string BuildInstanceClassName (Type instanceType) - { - var instance = instanced.First(i => i.Type == instanceType); - return BuildJSInteropInstanceClassName(instance); - } - - private bool IsInstanced (MethodMeta method) - { - return instanced.Any(i => i.Methods.Contains(method)); - } + private string Comma => builder[^1] == '{' ? "" : ","; } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs index 53d93589..e05ff893 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs @@ -2,12 +2,12 @@ namespace Bootsharp.Publish; internal sealed class DeclarationGenerator (Preferences prefs) { - private readonly MethodDeclarationGenerator methodsGenerator = new(prefs); + private readonly MemberDeclarationGenerator membersGenerator = new(prefs); private readonly TypeDeclarationGenerator typesGenerator = new(prefs); public string Generate (SolutionInspection inspection) => JoinLines(0, """import type { Event } from "./event";""", typesGenerator.Generate(inspection), - methodsGenerator.Generate(inspection) + membersGenerator.Generate(inspection) ) + "\n"; } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs new file mode 100644 index 00000000..7d4d0897 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs @@ -0,0 +1,90 @@ +using System.Text; + +namespace Bootsharp.Publish; + +internal sealed class MemberDeclarationGenerator (Preferences prefs) +{ + private readonly StringBuilder builder = new(); + private readonly TypeSyntaxBuilder typeBuilder = new(prefs); + + private MemberMeta member => members[index]; + private MemberMeta? prevMember => index == 0 ? null : members[index - 1]; + private MemberMeta? nextMember => index == members.Length - 1 ? null : members[index + 1]; + + private MemberMeta[] members = null!; + private int index; + + public string Generate (SolutionInspection inspection) + { + members = inspection.StaticMethods + .Concat(inspection.StaticInterfaces.SelectMany(i => i.Members)) + .OrderBy(m => m.JSSpace).ToArray(); + for (index = 0; index < members.Length; index++) + DeclareMember(); + return builder.ToString(); + } + + private void DeclareMember () + { + if (ShouldOpenNamespace()) OpenNamespace(); + switch (member) + { + case EventMeta e: DeclareEvent(e); break; + case PropertyMeta p: DeclareProperty(p); break; + case MethodMeta { Interop: InteropKind.Export } m: DeclareMethodExport(m); break; + case MethodMeta { Interop: InteropKind.Import } m: DeclareMethodImport(m); break; + } + if (ShouldCloseNamespace()) CloseNamespace(); + } + + private bool ShouldOpenNamespace () + { + if (prevMember is null) return true; + return prevMember.JSSpace != member.JSSpace; + } + + private void OpenNamespace () + { + builder.Append($"\nexport namespace {member.JSSpace} {{"); + } + + private bool ShouldCloseNamespace () + { + if (nextMember is null) return true; + return nextMember.JSSpace != member.JSSpace; + } + + private void CloseNamespace () + { + builder.Append("\n}"); + } + + private void DeclareProperty (PropertyMeta prop) + { + builder.Append($"\n export {(prop.CanGet && !prop.CanSet ? "const" : "let")} {prop.JSName}: "); + builder.Append(typeBuilder.Build(prop.Value.Type.Clr, prop.Value.Nullability)); + if (prop.Value.Nullable) builder.Append(" | null"); + builder.Append(';'); + } + + private void DeclareMethodExport (MethodMeta method) + { + builder.Append($"\n export function {method.JSName}("); + builder.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}")); + builder.Append($"): {typeBuilder.BuildReturn(method)};"); + } + + private void DeclareMethodImport (MethodMeta method) + { + builder.Append($"\n export let {method.JSName}: ("); + builder.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}")); + builder.Append($") => {typeBuilder.BuildReturn(method)};"); + } + + private void DeclareEvent (EventMeta @event) + { + builder.Append($"\n export const {@event.JSName}: Event<["); + builder.AppendJoin(", ", @event.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}")); + builder.Append("]>;"); + } +} diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MethodDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MethodDeclarationGenerator.cs deleted file mode 100644 index aee8df88..00000000 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MethodDeclarationGenerator.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Text; - -namespace Bootsharp.Publish; - -internal sealed class MethodDeclarationGenerator (Preferences prefs) -{ - private readonly StringBuilder builder = new(); - private readonly TypeSyntaxBuilder typeBuilder = new(prefs); - - private MethodMeta method => methods[index]; - private MethodMeta? prevMethod => index == 0 ? null : methods[index - 1]; - private MethodMeta? nextMethod => index == methods.Length - 1 ? null : methods[index + 1]; - - private MethodMeta[] methods = null!; - private int index; - - public string Generate (SolutionInspection inspection) - { - methods = inspection.StaticMethods - .Concat(inspection.StaticInterfaces.SelectMany(i => i.Methods)) - .OrderBy(m => m.JSSpace).ToArray(); - for (index = 0; index < methods.Length; index++) - DeclareMethod(); - return builder.ToString(); - } - - private void DeclareMethod () - { - if (ShouldOpenNamespace()) OpenNamespace(); - if (method.Kind == MethodKind.Invokable) DeclareInvokable(); - else if (method.Kind == MethodKind.Function) DeclareFunction(); - else DeclareEvent(); - if (ShouldCloseNamespace()) CloseNamespace(); - } - - private bool ShouldOpenNamespace () - { - if (prevMethod is null) return true; - return prevMethod.JSSpace != method.JSSpace; - } - - private void OpenNamespace () - { - builder.Append($"\nexport namespace {method.JSSpace} {{"); - } - - private bool ShouldCloseNamespace () - { - if (nextMethod is null) return true; - return nextMethod.JSSpace != method.JSSpace; - } - - private void CloseNamespace () - { - builder.Append("\n}"); - } - - private void DeclareInvokable () - { - builder.Append($"\n export function {method.JSName}("); - builder.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}")); - builder.Append($"): {typeBuilder.BuildReturn(method)};"); - } - - private void DeclareFunction () - { - builder.Append($"\n export let {method.JSName}: ("); - builder.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}")); - builder.Append($") => {typeBuilder.BuildReturn(method)};"); - } - - private void DeclareEvent () - { - builder.Append($"\n export const {method.JSName}: Event<["); - builder.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}")); - builder.Append("]>;"); - } -} diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs index 74740dd9..95f397c3 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs @@ -65,17 +65,6 @@ private void CloseNamespace () AppendLine("}", 0); } - private void DeclareInterface () - { - AppendLine($"export interface {BuildTypeName(type)}", indent); - AppendExtensions(); - builder.Append(" {"); - if (instanced.FirstOrDefault(i => i.Type == type) is { } inst) - AppendInstancedMethods(inst); - else AppendProperties(); - AppendLine("}", indent); - } - private void DeclareEnum () { AppendLine($"export enum {type.Name} {{", indent); @@ -86,9 +75,21 @@ private void DeclareEnum () AppendLine("}", indent); } - private string GetNamespace (Type type) + private void DeclareInterface () { - return BuildJSSpace(type, prefs); + AppendLine($"export interface {BuildTypeName(type)}", indent); + AppendExtensions(); + builder.Append(" {"); + if (instanced.FirstOrDefault(i => i.Type == type) is { } inst) + foreach (var member in inst.Members) + switch (member) + { + case EventMeta e: AppendInstancedEvent(e); break; + case PropertyMeta p: AppendInstancedProperty(p); break; + case MethodMeta m: AppendInstancedFunction(m); break; + } + else AppendProperties(); + AppendLine("}", indent); } private void AppendExtensions () @@ -103,31 +104,28 @@ private void AppendExtensions () private void AppendProperties () { var flags = BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance; - foreach (var property in type.GetProperties(flags)) - if (IsAutoProperty(property) || type.IsInterface) - AppendProperty(property); + foreach (var prop in type.GetProperties(flags)) + if (prop.GetMethod != null && prop.GetIndexParameters().Length == 0) + AppendProperty(ToFirstLower(prop.Name), prop.PropertyType, GetNullability(prop)); } - private void AppendProperty (PropertyInfo property) + private void AppendProperty (string name, Type type, NullabilityInfo? nullability) { - AppendLine(ToFirstLower(property.Name), indent + 1); - var nullability = GetNullability(property); - if (IsNullable(property.PropertyType, nullability)) builder.Append('?'); + AppendLine(name, indent + 1); + if (IsNullable(type, nullability)) builder.Append('?'); builder.Append(": "); - if (property.PropertyType.IsGenericTypeParameter) builder.Append(property.PropertyType.Name); - else builder.Append(typeBuilder.Build(property.PropertyType, nullability)); + if (type.IsGenericTypeParameter) builder.Append(type.Name); + else builder.Append(typeBuilder.Build(type, nullability)); builder.Append(';'); } - private void AppendInstancedMethods (InterfaceMeta instanced) + private void AppendInstancedProperty (PropertyMeta prop) { - foreach (var meta in instanced.Methods) - if (meta.Kind == MethodKind.Event) - AppendInstancedEvent(meta); - else AppendInstancedFunction(meta); + var name = prop.CanGet && !prop.CanSet ? $"readonly {prop.JSName}" : prop.JSName; + AppendProperty(name, prop.Value.Type.Clr, prop.Value.Nullability); } - private void AppendInstancedEvent (MethodMeta meta) + private void AppendInstancedEvent (EventMeta meta) { AppendLine(meta.JSName, indent + 1); builder.Append(": Event<["); @@ -165,4 +163,9 @@ private string BuildTypeName (Type type) var args = string.Join(", ", type.GetGenericArguments().Select(BuildTypeName)); return $"{name}<{args}>"; } + + private string GetNamespace (Type type) + { + return BuildJSSpace(type, prefs); + } } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs index a0d02f83..38af90ad 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs @@ -14,8 +14,8 @@ public string BuildArg (ArgumentMeta arg) public string BuildReturn (MethodMeta method) { - var nil = method.ReturnValue.Nullable ? " | null" : ""; - return Build(method.ReturnValue.Type.Clr, method.ReturnValue.Nullability) + nil; + var nil = method.Value.Nullable ? " | null" : ""; + return Build(method.Value.Type.Clr, method.Value.Nullability) + nil; } public string Build (Type type, NullabilityInfo? nullability) diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 0fade314..6297c64e 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.74 + 0.8.0-alpha.90 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/test/cs/Test.Types/Interfaces/ExportedInstanced.cs b/src/js/test/cs/Test.Types/Interfaces/ExportedInstanced.cs index 97f1ef28..c2e30668 100644 --- a/src/js/test/cs/Test.Types/Interfaces/ExportedInstanced.cs +++ b/src/js/test/cs/Test.Types/Interfaces/ExportedInstanced.cs @@ -4,11 +4,13 @@ namespace Test.Types; public class ExportedInstanced (string instanceArg) : IExportedInstanced { + public Record? Record { get; set; } + public string GetInstanceArg () => instanceArg; - public async Task GetVehicleIdAsync (Vehicle vehicle) + public async Task GetRecordIdAsync (Record record) { await Task.Delay(1); - return vehicle.Id; + return record.Id; } } diff --git a/src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs b/src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs index 1a693656..2a7c2601 100644 --- a/src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs +++ b/src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs @@ -4,6 +4,8 @@ namespace Test.Types; public class ExportedStatic : IExportedStatic { + public Record? Record { get; set; } + public async Task GetInstanceAsync (string arg) { await Task.Delay(1); diff --git a/src/js/test/cs/Test.Types/Interfaces/IExportedInstanced.cs b/src/js/test/cs/Test.Types/Interfaces/IExportedInstanced.cs index 26b82d7f..0e1fb6eb 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IExportedInstanced.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IExportedInstanced.cs @@ -4,6 +4,7 @@ namespace Test.Types; public interface IExportedInstanced { + Record? Record { get; set; } string GetInstanceArg (); - Task GetVehicleIdAsync (Vehicle vehicle); + Task GetRecordIdAsync (Record record); } diff --git a/src/js/test/cs/Test.Types/Interfaces/IExportedStatic.cs b/src/js/test/cs/Test.Types/Interfaces/IExportedStatic.cs index daf4e068..5bde3d25 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IExportedStatic.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IExportedStatic.cs @@ -4,5 +4,6 @@ namespace Test.Types; public interface IExportedStatic { + Record? Record { get; set; } Task GetInstanceAsync (string arg); } diff --git a/src/js/test/cs/Test.Types/Interfaces/IImportedInstanced.cs b/src/js/test/cs/Test.Types/Interfaces/IImportedInstanced.cs index 7ed037c5..9a1ddb71 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IImportedInstanced.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IImportedInstanced.cs @@ -4,6 +4,7 @@ namespace Test.Types; public interface IImportedInstanced { + Record? Record { get; set; } string GetInstanceArg (); - Task GetVehicleIdAsync (Vehicle vehicle); + Task GetRecordIdAsync (Record record); } diff --git a/src/js/test/cs/Test.Types/Interfaces/IImportedStatic.cs b/src/js/test/cs/Test.Types/Interfaces/IImportedStatic.cs index 55bd1ac4..f6fa7912 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IImportedStatic.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IImportedStatic.cs @@ -4,5 +4,6 @@ namespace Test.Types; public interface IImportedStatic { + Record? Record { get; set; } Task GetInstanceAsync (string arg); } diff --git a/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs b/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs new file mode 100644 index 00000000..7786f071 --- /dev/null +++ b/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using Bootsharp; + +namespace Test.Types; + +public static class Interfaces +{ + [JSInvokable] + public static async Task GetImportedArgAndRecordIdAsync (Record record, string arg) + { + var instance = await GetImportedStatic().GetInstanceAsync(arg); + return await instance.GetRecordIdAsync(record) + instance.GetInstanceArg(); + } + + [JSInvokable] + public static string GetImportedStaticRecordIdAndSet (Record record) + { + var imported = GetImportedStatic(); + var currentRecordId = imported.Record?.Id ?? ""; + imported.Record = record; + return currentRecordId; + } + + [JSInvokable] + public static async Task GetImportedInstanceArgAndRecordIdAsync (Record record, string arg) + { + var instance = await GetImportedStatic().GetInstanceAsync(arg); + var currentRecordId = instance.Record?.Id ?? ""; + instance.Record = record; + return instance.GetInstanceArg() + currentRecordId + instance.Record.Id; + } + + [JSInvokable] + public static async Task GetImportedArgsAndFinalize (string arg1, string arg2) + { + var imported = GetImportedStatic(); + var instance1 = await imported.GetInstanceAsync(arg1); + var instance2 = await imported.GetInstanceAsync(arg2); + var result = new[] { instance1.GetInstanceArg(), instance2.GetInstanceArg() }; + instance1 = null!; + instance2 = null!; + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + return result; + } + + private static IImportedStatic GetImportedStatic () + { + return (IImportedStatic)Bootsharp.Interfaces.Imports[typeof(IImportedStatic)].Instance; + } +} diff --git a/src/js/test/cs/Test.Types/Record.cs b/src/js/test/cs/Test.Types/Record.cs new file mode 100644 index 00000000..684431fe --- /dev/null +++ b/src/js/test/cs/Test.Types/Record.cs @@ -0,0 +1,3 @@ +namespace Test.Types; + +public record Record (string Id); diff --git a/src/js/test/cs/Test.Types/Vehicle/Registry.cs b/src/js/test/cs/Test.Types/Vehicle/Registry.cs index 6a1bbaae..a7d6b451 100644 --- a/src/js/test/cs/Test.Types/Vehicle/Registry.cs +++ b/src/js/test/cs/Test.Types/Vehicle/Registry.cs @@ -17,6 +17,9 @@ public partial class Registry [JSInvokable] public static Vehicle?[]? EchoVehicles (Vehicle?[]? value) => value; + [JSInvokable] + public static Record?[]? EchoRecords (Record?[]? value) => value; + [JSInvokable] public static float CountTotalSpeed () { diff --git a/src/js/test/cs/Test/Program.cs b/src/js/test/cs/Test/Program.cs index 4be5a95f..15d64a7d 100644 --- a/src/js/test/cs/Test/Program.cs +++ b/src/js/test/cs/Test/Program.cs @@ -1,5 +1,4 @@ using System; -using System.Threading.Tasks; using Bootsharp; using Bootsharp.Inject; using Microsoft.Extensions.DependencyInjection; @@ -27,35 +26,4 @@ public static void Main () [JSFunction] public static partial void OnMainInvoked (); - - [JSInvokable] - public static async Task GetExportedArgAndVehicleIdAsync (Vehicle vehicle, string arg) - { - var exported = services.GetService()!; - var instance = await exported.GetInstanceAsync(arg); - return await instance.GetVehicleIdAsync(vehicle) + instance.GetInstanceArg(); - } - - [JSInvokable] - public static async Task GetImportedArgAndVehicleIdAsync (Vehicle vehicle, string arg) - { - var imported = services.GetService()!; - var instance = await imported.GetInstanceAsync(arg); - return await instance.GetVehicleIdAsync(vehicle) + instance.GetInstanceArg(); - } - - [JSInvokable] - public static async Task GetImportedArgsAndFinalize (string arg1, string arg2) - { - var imported = services.GetService()!; - var instance1 = await imported.GetInstanceAsync(arg1); - var instance2 = await imported.GetInstanceAsync(arg2); - var result = new[] { instance1.GetInstanceArg(), instance2.GetInstanceArg() }; - instance1 = null!; - instance2 = null!; - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - return result; - } } diff --git a/src/js/test/cs/Test/Serialization.cs b/src/js/test/cs/Test/Serialization.cs index c99f97a1..d74bc954 100644 --- a/src/js/test/cs/Test/Serialization.cs +++ b/src/js/test/cs/Test/Serialization.cs @@ -46,10 +46,19 @@ public readonly record struct Union public readonly record struct ItemA (string? String, IReadOnlyDictionary? Map); public readonly record struct ItemB (string[] Strings, IReadOnlyCollection Times, IReadOnlyList? Ints); +public sealed record Computed +{ + public required string Id { get; init; } + public required int Count { get; init; } + public string Summary => $"{Id}:{Count}"; +} + public static class Serialization { [JSInvokable] public static Primitives?[]? EchoPrimitives (Primitives?[]? value) => value; [JSInvokable] public static Union?[]? EchoUnions (Union?[]? value) => value; + [JSInvokable] public static Computed EchoComputed (Computed value) => value; + [JSInvokable] public static Computed?[]? EchoComputedArray (Computed?[]? value) => value; [JSInvokable] public static byte[]? EchoBytes (byte[]? value) => value; [JSInvokable] public static int[]? EchoIntArray (int[]? value) => value; [JSInvokable] public static double[]? EchoDoubleArray (double[]? value) => value; diff --git a/src/js/test/spec/interop.spec.ts b/src/js/test/spec/interop.spec.ts index f25ad598..a91fda77 100644 --- a/src/js/test/spec/interop.spec.ts +++ b/src/js/test/spec/interop.spec.ts @@ -207,39 +207,56 @@ describe("while bootsharp is booted", () => { it("can interop with imported interfaces", async () => { class Imported { constructor(private arg: string) { } + record: Test.Types.Record = { id: "foo" }; getInstanceArg() { return this.arg; } - async getVehicleIdAsync(vehicle: Test.Types.Vehicle) { + async getRecordIdAsync(record: Test.Types.Record) { await new Promise(res => setTimeout(res, 1)); - return vehicle.id; + return record.id; } } + Test.Types.ImportedStatic.record = { id: "baz" }; Test.Types.ImportedStatic.getInstanceAsync = async (arg) => { await new Promise(res => setTimeout(res, 1)); return new Imported(arg); }; - const result1 = await Test.Program.getImportedArgAndVehicleIdAsync({ id: "foo", maxSpeed: 0 }, "bar"); - const result2 = await Test.Program.getImportedArgAndVehicleIdAsync({ id: "baz", maxSpeed: 0 }, "nya"); + const result1 = await Test.Types.Interfaces.getImportedArgAndRecordIdAsync({ id: "foo" }, "bar"); + const result2 = await Test.Types.Interfaces.getImportedArgAndRecordIdAsync({ id: "baz" }, "nya"); expect(result1).toStrictEqual("foobar"); expect(result2).toStrictEqual("baznya"); + expect(Test.Types.Interfaces.getImportedStaticRecordIdAndSet({ id: "qux" })).toStrictEqual("baz"); + expect(Test.Types.ImportedStatic.record).toStrictEqual({ id: "qux" }); + expect(await Test.Types.Interfaces.getImportedInstanceArgAndRecordIdAsync({ id: "zip" }, "qux")) + .toStrictEqual("quxfoozip"); }); it("can interop with exported interfaces", async () => { + const record = { id: "foo" }; + Test.Types.ExportedStatic.record = record; + expect(Test.Types.ExportedStatic.record).toStrictEqual(record); + Test.Types.ExportedStatic.record = { id: "bar" }; + expect(Test.Types.ExportedStatic.record).toStrictEqual({ id: "bar" }); + Test.Types.ExportedStatic.record = null; + expect(Test.Types.ExportedStatic.record).toBeNull(); + const exported = await Test.Types.ExportedStatic.getInstanceAsync("bar"); expect(exported.getInstanceArg()).toStrictEqual("bar"); - expect(await exported.getVehicleIdAsync({ id: "foo", maxSpeed: 0 })).toStrictEqual("foo"); - expect(await Test.Program.getExportedArgAndVehicleIdAsync({ id: "foo", maxSpeed: 0 }, "bar")).toStrictEqual("foobar"); + expect(await exported.getRecordIdAsync({ id: "foo" })).toStrictEqual("foo"); + expect(exported.record).toBeNull(); + exported.record = { id: "qux" }; + expect(exported.record).toStrictEqual({ id: "qux" }); }); it("releases interface instances after use", async () => { class Imported { constructor(private arg: string) { } + record?: Test.Types.Record; getInstanceArg() { return this.arg; } - async getVehicleIdAsync(vehicle: Test.Types.Vehicle) { + async getRecordIdAsync(record: Test.Types.Record) { await new Promise(res => setTimeout(res, 1)); - return vehicle.id; + return record.id; } } Test.Types.ImportedStatic.getInstanceAsync = async (arg) => new Imported(arg); - expect(await Test.Program.getImportedArgsAndFinalize("qux", "fox")).toStrictEqual(["qux", "fox"]); - expect(await Test.Program.getImportedArgsAndFinalize("zip", "zap")).toStrictEqual(["zip", "zap"]); + expect(await Test.Types.Interfaces.getImportedArgsAndFinalize("qux", "fox")).toStrictEqual(["qux", "fox"]); + expect(await Test.Types.Interfaces.getImportedArgsAndFinalize("zip", "zap")).toStrictEqual(["zip", "zap"]); }); it("empty string of a struct is transferred correctly", () => { const id = Test.Types.Registry.getWithEmptyId().id; diff --git a/src/js/test/spec/serialization.spec.ts b/src/js/test/spec/serialization.spec.ts index 5227c841..6befe0ed 100644 --- a/src/js/test/spec/serialization.spec.ts +++ b/src/js/test/spec/serialization.spec.ts @@ -47,7 +47,16 @@ describe("serialization", () => { expect(Test.Serialization.echoUnions(undefined)).toBeNull(); }); + it("computes expression properties on the C# side", () => { + expect(Test.Serialization.echoComputed({ id: "foo", count: 7, summary: "ignored" })) + .toStrictEqual({ id: "foo", count: 7, summary: "foo:7" }); + expect(Test.Serialization.echoComputedArray([{ id: "bar", count: 3, summary: "ignored" }, null])) + .toStrictEqual([{ id: "bar", count: 3, summary: "bar:3" }, null]); + }); + it("can echo vehicles", async () => { + expect(Test.Types.Registry.echoRecords([{ id: "foo" }, null])) + .toStrictEqual([{ id: "foo" }, null]); expect(Test.Types.Registry.echoVehicles([{ id: "foo", maxSpeed: 1 }, null])) .toStrictEqual([{ id: "foo", maxSpeed: 1 }, null]); expect(Test.Types.Registry.echoRegistry({ From 2772c301ad4fa898ef4a09a5f0cd7915b5b96284 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Tue, 21 Apr 2026 20:21:34 +0300 Subject: [PATCH 2/3] update docs --- docs/guide/interop-instances.md | 21 ++++++++++++++++++--- docs/guide/interop-interfaces.md | 25 +++++-------------------- docs/guide/serialization.md | 22 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/docs/guide/interop-instances.md b/docs/guide/interop-instances.md index c440fb42..8f3818ac 100644 --- a/docs/guide/interop-instances.md +++ b/docs/guide/interop-instances.md @@ -3,11 +3,21 @@ When an interface is supplied as argument or return type of an interop method, instead of serializing it as value, Bootsharp will instead generate an instance binding, eg: ```csharp -public interface IExported { string GetFromCSharp (); } -public interface IImported { string GetFromJavaScript (); } +public interface IExported +{ + string Value { get; set; } + string GetFromCSharp (); +} + +public interface IImported +{ + string Value { get; set; } + string GetFromJavaScript (); +} public class Exported : IExported { + public string Value { get; set; } = "cs"; public string GetFromCSharp () => "cs"; } @@ -18,13 +28,16 @@ public static partial class Factory } var imported = Factory.GetImported(); -imported.GetFromJavaScript(); //returns "js" +imported.GetFromJavaScript(); // returns "js" +imported.value = "updated"; // invokes the JS setter +_ = imported.value; // invokes the JS getter ``` ```ts import { Factory, IImported } from "bootsharp"; class Imported implements IImported { + value = "js"; getFromJavaScript() { return "js"; } } @@ -32,6 +45,8 @@ Factory.getImported = () => new Imported(); const exported = Factory.getExported(); exported.getFromCSharp(); // returns "cs" +exported.value = "updated"; // invokes the C# setter +_ = exported.value; // invokes the C# getter ``` Interop instances are subject to the following limitations: diff --git a/docs/guide/interop-interfaces.md b/docs/guide/interop-interfaces.md index 07b88e36..e148ca61 100644 --- a/docs/guide/interop-interfaces.md +++ b/docs/guide/interop-interfaces.md @@ -7,8 +7,8 @@ For example, say we have a JavaScript UI (frontend) that needs to be notified wh ```csharp interface IFrontend { + bool IsMuted { get; set; } void NotifyDataChanged (Data data); - bool IsMuted (); } ``` @@ -24,8 +24,8 @@ Bootsharp will automatically implement the interface in C#, wiring it to JavaScr ```ts export namespace Frontend { + export let isMuted: boolean; export const onDataChanged: Event<[Data]>; - export let isMuted: () => boolean; } ``` @@ -34,6 +34,7 @@ Now, say we want to provide an API for the frontend to request a mutation of the ```csharp interface IBackend { + Data? Current { get; set; } void AddData (Data data); } ``` @@ -46,27 +47,11 @@ Export the interface to JavaScript: ])] ``` -This will generate the following implementation: - -```csharp -public class JSBackend -{ - private static IBackend handler = null!; - - public JSBackend (IBackend handler) - { - JSBackend.handler = handler; - } - - [JSInvokable] - public static void AddData (Data data) => handler.AddData(data); -} -``` - -— which will produce the following spec to be consumed on the JavaScript side: +This will produce the following spec to be consumed on the JavaScript side: ```ts export namespace Backend { + export let current: Data | null; export function addData(data: Data): void; } ``` diff --git a/docs/guide/serialization.md b/docs/guide/serialization.md index 6d7910de..29b9a72f 100644 --- a/docs/guide/serialization.md +++ b/docs/guide/serialization.md @@ -98,3 +98,25 @@ import { Program } from "bootsharp"; const map = Program.map(["foo", "bar"], [0, 7]); console.log(map.get("bar")); // 7 ``` + +## Computed Properties + +Computed C# properties are evaluated and written to the JS objects on serialization. For example: + +```csharp +public record Order +{ + public required string Id { get; init; } + public required int Revision { get; init; } + public string Version => $"{Id}:{Revision}"; +} +``` + +When an `Order` crosses the C# -> JavaScript boundary, Bootsharp reads `Version` on the C# side and writes the result to the JavaScript object as a regular persisted value: + +```ts +const order = Orders.getCurrent(); +console.log(order.version); // "A:7" +``` + +The value is computed only while the C# object is being serialized. After that, it's just a normal JavaScript property on the received object. From 7f732be5a0ae0e927d5daff8905d21c36d4651e2 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Tue, 21 Apr 2026 20:25:34 +0300 Subject: [PATCH 3/3] fmt --- src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs index c0847d5b..b9360164 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs @@ -701,13 +701,13 @@ public void ComputedPropertiesAreIncluded () { AddAssembly(WithClass( """ - public record Foo + public record Foo { - public bool Boo => true; - public bool SetOnly { set { } } - public bool this[int index] => true; + public bool Boo => true; + public bool SetOnly { set { } } + public bool this[int index] => true; } - + [JSInvokable] public static Foo Bar () => default; """)); Execute();