diff --git a/README.md b/README.md index ec21b0d6..d33add46 100644 --- a/README.md +++ b/README.md @@ -318,14 +318,14 @@ TypeShim aims to continue to broaden its type support. Suggestions and contribut | `ArraySegment`| `MemoryView`| 🚧 | | | `ArraySegment`| `MemoryView`| 🚧 | | | `Task` | `Promise` | ✅ | * [Only supported .NET types](https://learn.microsoft.com/en-us/aspnet/core/client-side/dotnet-interop/?view=aspnetcore-10.0#type-mappings) | -| `Action` | `Function` | 🚧 | | -| `Action` | `Function` | 🚧 | | -| `Action` | `Function` | 🚧 | | -| `Action` | `Function` | 🚧 | | -| `Func` | `Function` | 🚧 | | -| `Func` | `Function` | 🚧 | | -| `Func` | `Function`| 🚧 | | -| `Func` | `Function` | 🚧 | | +| `Action` | `Function` | ✅ | | +| `Action` | `Function` | ✅ | | +| `Action` | `Function` | ✅ | | +| `Action` | `Function` | ✅ | | +| `Func` | `Function` | ✅ | | +| `Func` | `Function` | ✅ | | +| `Func` | `Function`| ✅ | | +| `Func` | `Function` | ✅ | | *For `[TSExport]` classes diff --git a/TypeShim.E2E/TypeShim.E2E.Wasm/ArrayPropertiesClass.cs b/TypeShim.E2E/TypeShim.E2E.Wasm/ArrayPropertiesClass.cs index c54f69a1..0ce263f9 100644 --- a/TypeShim.E2E/TypeShim.E2E.Wasm/ArrayPropertiesClass.cs +++ b/TypeShim.E2E/TypeShim.E2E.Wasm/ArrayPropertiesClass.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices.JavaScript; -namespace TypeShim.Sample; +namespace TypeShim.E2E.Wasm; [TSExport] public class ArrayPropertiesClass diff --git a/TypeShim.E2E/TypeShim.E2E.Wasm/DelegatesClass.cs b/TypeShim.E2E/TypeShim.E2E.Wasm/DelegatesClass.cs new file mode 100644 index 00000000..a9adb39b --- /dev/null +++ b/TypeShim.E2E/TypeShim.E2E.Wasm/DelegatesClass.cs @@ -0,0 +1,112 @@ +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; + +namespace TypeShim.E2E.Wasm; + +[TSExport] +public class DelegatePropertyClass +{ + public required Func ExportedClassFuncProperty { get; set; } +} + +[TSExport] +public class DelegatesClass +{ + public Func? FuncBoolIntProperty { get; set; } + public required Func FuncCharProperty { get; set; } + + public void InvokeVoidAction(Action action) + { + action(); + } + + public void InvokeStringAction(Action action) + { + action("Hello"); + } + + public void InvokeInt32Action(Action action) + { + action(42); + } + + public void InvokeBoolAction(Action action) + { + action(true); + } + public void InvokeCharAction(Action action) + { + action('Z'); + } + + public Func GetCharCharFunc() + { + return (char c) => (char)(c + 1); + } + + public void InvokeBool2Action(Action action) + { + action(true, false); + } + + public void InvokeBool3Action(Action action) + { + action(true, false, true); + } + + public string InvokeStringFunc(Func func) + { + return func(); + } + + public int InvokeInt32Func(Func func) + { + return func(); + } + + public bool InvokeBoolFunc(Func func) + { + return func(); + } + + public bool InvokeBool2Func(Func func) + { + return func(true); + } + + public void InvokeExportedClassAction(Action action) + { + action(new ExportedClass { Id = 100 }); + } + + public Func GetExportedClassFunc() + { + return () => new ExportedClass { Id = 200 }; + } + + public Func GetBoolIntStringExportFunc() + { + return (bool b, int a, string c) => new ExportedClass { Id = b ? a : c.Length }; + } + + public Func GetBoolIntCharExportFunc() + { + return (bool b, int a, char c) => new ExportedClass { Id = b ? a : c }; + } + + public Func GetBoolIntExportCharFunc() + { + return (bool b, int a, ExportedClass c) => b ? (char)a : (char)c.Id; + } + + public Func GetExportedClassExportedClassFunc() + { + return (ExportedClass classIn) => classIn; + } + + public ExportedClass InvokeExportedClassExportedClassFunc(Func func, Func paramFunc) + { + return func(paramFunc()); + } +} diff --git a/TypeShim.E2E/TypeShim.E2E.Wasm/ExportedClass.cs b/TypeShim.E2E/TypeShim.E2E.Wasm/ExportedClass.cs index e8117952..dfb0149e 100644 --- a/TypeShim.E2E/TypeShim.E2E.Wasm/ExportedClass.cs +++ b/TypeShim.E2E/TypeShim.E2E.Wasm/ExportedClass.cs @@ -1,4 +1,4 @@ -namespace TypeShim.Sample; +namespace TypeShim.E2E.Wasm; [TSExport] public class ExportedClass // for referencing an exported class diff --git a/TypeShim.E2E/TypeShim.E2E.Wasm/SimplePropertiesTest.cs b/TypeShim.E2E/TypeShim.E2E.Wasm/SimplePropertiesTest.cs index 0e6b9939..d1c7bea7 100644 --- a/TypeShim.E2E/TypeShim.E2E.Wasm/SimplePropertiesTest.cs +++ b/TypeShim.E2E/TypeShim.E2E.Wasm/SimplePropertiesTest.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.InteropServices.JavaScript; -namespace TypeShim.Sample; +namespace TypeShim.E2E.Wasm; [TSExport] public class SimplePropertiesTest @@ -14,6 +14,7 @@ public class SimplePropertiesTest public bool BoolProperty { get; set; } public string StringProperty { get; set; } = string.Empty; public char CharProperty { get; set; } + public char? CharNullableProperty { get; set; } public double DoubleProperty { get; set; } public float FloatProperty { get; set; } public DateTime DateTimeProperty { get; set; } diff --git a/TypeShim.E2E/TypeShim.E2E.Wasm/SimpleReturnMethodsClass.cs b/TypeShim.E2E/TypeShim.E2E.Wasm/SimpleReturnMethodsClass.cs index 184cdc24..c42514f5 100644 --- a/TypeShim.E2E/TypeShim.E2E.Wasm/SimpleReturnMethodsClass.cs +++ b/TypeShim.E2E/TypeShim.E2E.Wasm/SimpleReturnMethodsClass.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; -namespace TypeShim.Sample; +namespace TypeShim.E2E.Wasm; [TSExport] public class SimpleReturnMethodsClass diff --git a/TypeShim.E2E/TypeShim.E2E.Wasm/TaskPropertiesClass.cs b/TypeShim.E2E/TypeShim.E2E.Wasm/TaskPropertiesClass.cs index 6cedaa15..5711ac99 100644 --- a/TypeShim.E2E/TypeShim.E2E.Wasm/TaskPropertiesClass.cs +++ b/TypeShim.E2E/TypeShim.E2E.Wasm/TaskPropertiesClass.cs @@ -2,7 +2,7 @@ using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; -namespace TypeShim.Sample; +namespace TypeShim.E2E.Wasm; [TSExport] public class TaskPropertiesClass diff --git a/TypeShim.E2E/TypeShim.E2E.Wasm/TaskReturnMethodsClass.cs b/TypeShim.E2E/TypeShim.E2E.Wasm/TaskReturnMethodsClass.cs new file mode 100644 index 00000000..49d327cf --- /dev/null +++ b/TypeShim.E2E/TypeShim.E2E.Wasm/TaskReturnMethodsClass.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; + +namespace TypeShim.E2E.Wasm; + +[TSExport] +public class TaskReturnMethodsClass +{ + public Task VoidTaskMethod() => Task.CompletedTask; + public Task Int32TaskMethod() => Task.FromResult(42); + public Task BoolTaskMethod() => Task.FromResult(true); + public Task StringTaskMethod() => Task.FromResult("Hello, from .NET Task"); + + public Task ExportedClassTaskMethod() => Task.FromResult(new ExportedClass() { Id = 420 }); +} diff --git a/TypeShim.E2E/vitest/src/array-properties.test.ts b/TypeShim.E2E/vitest/src/array-properties.test.ts index 4ba337fa..2948883e 100644 --- a/TypeShim.E2E/vitest/src/array-properties.test.ts +++ b/TypeShim.E2E/vitest/src/array-properties.test.ts @@ -28,4 +28,56 @@ describe('Array Properties Test', () => { testObject.IntArrayProperty[0] = 42; expect(testObject.IntArrayProperty).toStrictEqual(original); }); + + test('Initialized ExportedClass array property', () => { + expect(testObject.ExportedClassArrayProperty).toBeInstanceOf(Array); + expect(testObject.ExportedClassArrayProperty.length).toBe(1); + const item = testObject.ExportedClassArrayProperty[0]; + expect(item).toBeInstanceOf(ExportedClass); + expect(item.Id).toBe(exportedClass.Id); + // TODO: fix identity (https://github.com/ArcadeMode/TypeShim/issues/20) + // expect(item).toBe(exportedClass); + }); + + test('Initializer JSObject array property', () => { + expect(testObject.JSObjectArrayProperty).toBeInstanceOf(Array); + expect(testObject.JSObjectArrayProperty.length).toBe(3); + expect(testObject.JSObjectArrayProperty[0]).toMatchObject({ a: 1 }); + expect(testObject.JSObjectArrayProperty[1]).toMatchObject({ b: 2 }); + expect(testObject.JSObjectArrayProperty[2]).toMatchObject({ c: 3 }); + }); + + test('ExportedClass array property set with ExportedClass.Initializer', () => { + const exportedClassInitializer = { Id: 12345 }; + testObject.ExportedClassArrayProperty = [exportedClassInitializer]; + expect(testObject.ExportedClassArrayProperty).toBeInstanceOf(Array); + expect(testObject.ExportedClassArrayProperty.length).toBe(1); + const item = testObject.ExportedClassArrayProperty[0]; + expect(item).toBeInstanceOf(ExportedClass); + expect(item.Id).toBe(12345); + }); + + test('ExportedClass array property set with ExportedClass.Initializer', () => { + const exportedClassInitializer = { Id: 12345 }; + const testObject = new ArrayPropertiesClass({ + ByteArrayProperty: [], + IntArrayProperty: [], + StringArrayProperty: [], + DoubleArrayProperty: [], + JSObjectArrayProperty: [], + ObjectArrayProperty: [], + ExportedClassArrayProperty: [exportedClassInitializer], + }); + + expect(testObject.ExportedClassArrayProperty).toBeInstanceOf(Array); + expect(testObject.ExportedClassArrayProperty.length).toBe(1); + const item = testObject.ExportedClassArrayProperty[0]; + expect(item).toBeInstanceOf(ExportedClass); + expect(item.Id).toBe(12345); + }); + + test('Object from ObjectArrayProperty has reference equality', () => { + const item = testObject.ObjectArrayProperty[0]; + expect(item).toBe(exportedClass.instance); + }); }); \ No newline at end of file diff --git a/TypeShim.E2E/vitest/src/delegates.test.ts b/TypeShim.E2E/vitest/src/delegates.test.ts new file mode 100644 index 00000000..0649f803 --- /dev/null +++ b/TypeShim.E2E/vitest/src/delegates.test.ts @@ -0,0 +1,264 @@ +import { describe, test, expect, beforeEach } from 'vitest'; +import { DelegatePropertyClass, DelegatesClass, ExportedClass } from '@typeshim/e2e-wasm-lib'; + +describe('Delegates Test', () => { + let exportedClass: ExportedClass; + let testObject: DelegatesClass; + beforeEach(() => { + exportedClass = new ExportedClass({ Id: 2 }); + testObject = new DelegatesClass({ FuncBoolIntProperty: null, FuncCharProperty: () => 'A' }); + }); + + test('Set and Get FuncBoolIntProperty', async () => { + const func = (arg0: boolean) => { + return arg0 ? 1 : 0; + }; + testObject.FuncBoolIntProperty = func; + const retrievedFunc = testObject.FuncBoolIntProperty; + expect(retrievedFunc).not.toBeNull(); + expect(retrievedFunc!(true)).toBe(1); + expect(retrievedFunc!(false)).toBe(0); + }); + + test('Initialize FuncBoolIntProperty', async () => { + testObject = new DelegatesClass({ + FuncBoolIntProperty: (arg0: boolean) => { + return arg0 ? 1 : 0; + }, + FuncCharProperty: () => 'A' + }); + const retrievedFunc = testObject.FuncBoolIntProperty; + expect(retrievedFunc).not.toBeNull(); + expect(retrievedFunc!(true)).toBe(1); + expect(retrievedFunc!(false)).toBe(0); + }); + + test('Invoke Void Action', async () => { + let isInvoked = false; + testObject.InvokeVoidAction(() => { + isInvoked = true; + }); + expect(isInvoked).toBe(true); + }); + + test('Invoke String Action', async () => { + let receivedString = ""; + testObject.InvokeStringAction((arg0: string) => { + receivedString = arg0; + }); + expect(receivedString).toBe("Hello"); + }); + + test('Invoke Int32 Action', async () => { + let receivedInt = 0; + testObject.InvokeInt32Action((arg0: number) => { + receivedInt = arg0; + }); + expect(receivedInt).toBe(42); + }); + + test('Invoke Bool Action', async () => { + let receivedBool = false; + testObject.InvokeBoolAction((arg0: boolean) => { + receivedBool = arg0; + }); + expect(receivedBool).toBe(true); + }); + + test('Invoke Char Action', async () => { + let receivedChar = ''; + testObject.InvokeCharAction((arg0: string) => { + receivedChar = arg0; + }); + expect(receivedChar).toBeTypeOf('string'); + expect(receivedChar).toBe('Z'); + }); + + test('Get GetCharCharFunc', async () => { + const fn = testObject.GetCharCharFunc(); + const retVal = fn('D'); + expect(retVal).not.toBeNull(); + expect(retVal).toBeTypeOf('string'); + expect(retVal).toBe('E'); // char + 1 on CS side + }); + + test('Invoke Bool2 Action', async () => { + let receivedBool1 = false; + let receivedBool2 = false; + testObject.InvokeBool2Action((arg0: boolean, arg1: boolean) => { + receivedBool1 = arg0; + receivedBool2 = arg1; + }); + expect(receivedBool1).toBe(true); + expect(receivedBool2).toBe(false); + }); + + test('Invoke Bool3 Action', async () => { + let receivedBool1 = false; + let receivedBool2 = false; + let receivedBool3 = false; + testObject.InvokeBool3Action((arg0: boolean, arg1: boolean, arg2: boolean) => { + receivedBool1 = arg0; + receivedBool2 = arg1; + receivedBool3 = arg2; + }); + expect(receivedBool1).toBe(true); + expect(receivedBool2).toBe(false); + expect(receivedBool3).toBe(true); + }); + + test('Invoke String Func', async () => { + const result = testObject.InvokeStringFunc(() => { + return "Hello World"; + }); + expect(result).toBe("Hello World"); + }); + + test('Invoke Int32 Func', async () => { + const result = testObject.InvokeInt32Func(() => { + return 12345; + }); + expect(result).toBe(12345); + }); + + test('Invoke Bool Func', async () => { + const result = testObject.InvokeBoolFunc(() => { + return true; + }); + expect(result).toBe(true); + }); + + test('Invoke Bool2 Func', async () => { + const result = testObject.InvokeBool2Func((arg0: boolean) => { + return arg0; + }); + expect(result).toBe(true); + }); + + test('Invoke ExportedClass Action', async () => { + let receivedInstance: ExportedClass = null as any; + testObject.InvokeExportedClassAction((arg0: ExportedClass) => { + receivedInstance = arg0; + }); + expect(receivedInstance).not.toBeNull(); + expect(receivedInstance).toBeInstanceOf(ExportedClass); + expect(receivedInstance.Id).toBe(100); + }); + + test('Get ExportedClass Func', async () => { + const fn = testObject.GetExportedClassFunc(); + const exportedInstance = fn(); + expect(exportedInstance).not.toBeNull(); + expect(exportedInstance).toBeInstanceOf(ExportedClass); + expect(exportedInstance.Id).toBe(200); + }); + + test('Get ExportedClassExportedClassFunc Proxy', async () => { + const instance = new ExportedClass({ Id: 300 }); + const fn = testObject.GetExportedClassExportedClassFunc(); + const retVal = fn(instance); + expect(retVal).not.toBeNull(); + expect(retVal).toBeInstanceOf(ExportedClass); + expect(retVal.Id).toBe(300); + }); + + test('Get ExportedClassExportedClassFunc Initializer', async () => { + const instance = { Id: 250 }; + const fn = testObject.GetExportedClassExportedClassFunc(); + const retVal = fn(instance); + expect(retVal).not.toBeNull(); + expect(retVal).toBeInstanceOf(ExportedClass); + expect(retVal.Id).toBe(250); + }); + + test('Invoke Function Param with return value from another Function Param (C#)', async () => { + const retVal = testObject.InvokeExportedClassExportedClassFunc(testObject.GetExportedClassExportedClassFunc(), testObject.GetExportedClassFunc()); + expect(retVal).not.toBeNull(); + expect(retVal).toBeInstanceOf(ExportedClass); + expect(retVal.Id).toBe(200); + }); + + test('Invoke Function Param with return value from another Function Param (C#-JS)', async () => { + const retVal = testObject.InvokeExportedClassExportedClassFunc(testObject.GetExportedClassExportedClassFunc(), () => exportedClass); + expect(retVal).not.toBeNull(); + expect(retVal).toBeInstanceOf(ExportedClass); + expect(retVal.Id).toBe(exportedClass.Id); + // TODO: fix identity (https://github.com/ArcadeMode/TypeShim/issues/20) + //expect(retVal).toBe(exportedClass); + }); + + test('Initializer with FuncCharProperty', async () => { + const testObject2 = new DelegatesClass({ + FuncCharProperty: () => { + return 'X' + }, + FuncBoolIntProperty: null + }); + const retVal = testObject2.FuncCharProperty(); + expect(retVal).not.toBeNull(); + expect(retVal).toBeTypeOf('string'); + expect(retVal).toBe('X'); + }); + + test('GetBoolIntStringExportFunc', async () => { + const fn = testObject.GetBoolIntStringExportFunc(); + const retVal = fn(true, 3, "1234"); + expect(retVal).not.toBeNull(); + expect(retVal).toBeInstanceOf(ExportedClass); + expect(retVal.Id).toBe(3); + }); + + test('GetBoolIntStringExportFunc', async () => { + const fn = testObject.GetBoolIntCharExportFunc(); + const retVal = fn(false, 3, "A"); + expect(retVal).not.toBeNull(); + expect(retVal).toBeInstanceOf(ExportedClass); + expect(retVal.Id).toBe(65); // 'A' char code is 65, thats set on CS side + }); + + test('GetBoolIntExportCharFunc Proxy', async () => { + const fn = testObject.GetBoolIntExportCharFunc(); + const retVal = fn(false, 3, new ExportedClass({ Id: 65 })); + expect(retVal).not.toBeNull(); + expect(retVal).toBeTypeOf('string'); + expect(retVal).toBe('A'); // 'A' char code is 65, thats returned from CS + }); + + test('GetBoolIntExportCharFunc Initializer', async () => { + const fn = testObject.GetBoolIntExportCharFunc(); + const retVal = fn(false, 3, { Id: 66 }); + expect(retVal).not.toBeNull(); + expect(retVal).toBeTypeOf('string'); + expect(retVal).toBe('B'); // 'B' char code is 66, thats returned from CS + }); + + test('Property ExportedClassFuncProperty Proxy', async () => { + const delegatePropClass = new DelegatePropertyClass({ + ExportedClassFuncProperty: (arg0: ExportedClass | ExportedClass.Initializer) => arg0 as ExportedClass, + }); + const retVal = delegatePropClass.ExportedClassFuncProperty(new ExportedClass({ Id: 70 })); + expect(retVal).not.toBeNull(); + expect(retVal).toBeInstanceOf(ExportedClass); + expect(retVal.Id).toBe(70); + }); + + test('Property ExportedClassFuncProperty Proxy', async () => { + const delegatePropClass = new DelegatePropertyClass({ + ExportedClassFuncProperty: (arg0: ExportedClass | ExportedClass.Initializer) => arg0 as ExportedClass, + }); + const retVal = delegatePropClass.ExportedClassFuncProperty(new ExportedClass({ Id: 70 })); + expect(retVal).not.toBeNull(); + expect(retVal).toBeInstanceOf(ExportedClass); + expect(retVal.Id).toBe(70); + }); + + test('Property ExportedClassFuncProperty Initializer', async () => { + const delegatePropClass = new DelegatePropertyClass({ + ExportedClassFuncProperty: (arg0: ExportedClass | ExportedClass.Initializer) => arg0 as ExportedClass, + }); + const retVal = delegatePropClass.ExportedClassFuncProperty({ Id: 75 }); + expect(retVal).not.toBeNull(); + expect(retVal).toBeInstanceOf(ExportedClass); + expect(retVal.Id).toBe(75); + }); +}); \ No newline at end of file diff --git a/TypeShim.E2E/vitest/src/exception.test.ts b/TypeShim.E2E/vitest/src/exception.test.ts index ffdd42c5..8f122e33 100644 --- a/TypeShim.E2E/vitest/src/exception.test.ts +++ b/TypeShim.E2E/vitest/src/exception.test.ts @@ -18,7 +18,7 @@ describe('Task Properties Test', () => { TaskOfNIntProperty: Promise.resolve(42), TaskOfShortProperty: Promise.resolve(43), TaskOfIntProperty: Promise.resolve(44), - TaskOfLongProperty: Promise.resolve(45),// new Promise(resolve => setTimeout(() => resolve(45), 1000)), + TaskOfLongProperty: Promise.resolve(45), TaskOfBoolProperty: Promise.resolve(true), TaskOfCharProperty: Promise.resolve('B'), TaskOfStringProperty: Promise.resolve("Task String"), @@ -39,6 +39,7 @@ describe('Task Properties Test', () => { BoolProperty: true, StringProperty: "Test", CharProperty: 'A', + CharNullableProperty: null, DoubleProperty: 6.7, FloatProperty: 8.9, DateTimeProperty: dateNow, diff --git a/TypeShim.E2E/vitest/src/simple-properties.test.ts b/TypeShim.E2E/vitest/src/simple-properties.test.ts index 5e1f955b..1a28ccba 100644 --- a/TypeShim.E2E/vitest/src/simple-properties.test.ts +++ b/TypeShim.E2E/vitest/src/simple-properties.test.ts @@ -17,6 +17,7 @@ describe('Simple Properties Test', () => { BoolProperty: true, StringProperty: "Test", CharProperty: 'A', + CharNullableProperty: null, DoubleProperty: 6.7, FloatProperty: 8.9, DateTimeProperty: dateNow, @@ -36,6 +37,32 @@ describe('Simple Properties Test', () => { expect(testObject.ExportedClassProperty).toBeInstanceOf(ExportedClass); expect(testObject.ExportedClassProperty.Id).toBe(2); }); + test('Mutates ExportedClass property correctly', () => { + expect(testObject.ExportedClassProperty).toBeInstanceOf(ExportedClass); + expect(testObject.ExportedClassProperty.Id).toBe(2); + const newExportedClass = new ExportedClass({ Id: 99 }); + testObject.ExportedClassProperty = newExportedClass; + expect(testObject.ExportedClassProperty).toBeInstanceOf(ExportedClass); + expect(testObject.ExportedClassProperty.Id).toBe(99); + // TODO: fix identity (https://github.com/ArcadeMode/TypeShim/issues/20) + // expect(testObject.ExportedClassProperty).toBe(newExportedClass); + }); + test('Mutates ExportedClass property with Initializer', () => { + expect(testObject.ExportedClassProperty).toBeInstanceOf(ExportedClass); + expect(testObject.ExportedClassProperty.Id).toBe(2); + const exportedClassInitializer = { Id: 12345 }; + testObject.ExportedClassProperty = exportedClassInitializer; + expect(testObject.ExportedClassProperty).toBeInstanceOf(ExportedClass); + expect(testObject.ExportedClassProperty.Id).toBe(12345); + }); + test('Mutates ExportedClass property does not affect snapshot', () => { + const snapshot = SimplePropertiesTest.materialize(testObject); + expect(testObject.ExportedClassProperty).toBeInstanceOf(ExportedClass); + expect(testObject.ExportedClassProperty.Id).toBe(2); + const newExportedClass = new ExportedClass({ Id: 99 }); + testObject.ExportedClassProperty = newExportedClass; + expect(snapshot.ExportedClassProperty.Id).toBe(2); + }); test('Returns JSObject property by reference', () => { expect(testObject.JSObjectProperty).toBe(jsObject); const obj = testObject.JSObjectProperty as any; diff --git a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Constructors.cs b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Constructors.cs index 750883be..2aaf556d 100644 --- a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Constructors.cs +++ b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Constructors.cs @@ -32,7 +32,7 @@ public class C1 InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -90,7 +90,7 @@ public class C1(int p1, double p2) InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -148,7 +148,7 @@ public class C1() InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -165,7 +165,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P1 = (jsObject.GetPropertyAsInt32Nullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.GetPropertyAsInt32Nullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } [JSExport] @@ -195,7 +195,7 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P1 = (jsObject.GetPropertyAsInt32Nullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.GetPropertyAsInt32Nullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } } @@ -224,7 +224,7 @@ public class C1() InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -241,7 +241,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P1 = (jsObject.GetPropertyAsInt32Nullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.GetPropertyAsInt32Nullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } [JSExport] @@ -264,7 +264,7 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P1 = (jsObject.GetPropertyAsInt32Nullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.GetPropertyAsInt32Nullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } } @@ -293,7 +293,7 @@ public class C1(int p1, double p2) InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -310,7 +310,7 @@ public static object ctor([JSMarshalAs] int p1, [JSMarshalAs] int p1, [JSMarshalAs] - public static object ctor([JSMarshalAs] object? p1) - { - MyClass? typed_p1 = p1 != null ? MyClassInterop.FromObject(p1) : null; - return new C1(typed_p1); - } - [JSExport] - [return: JSMarshalAs] - public static int get_P1([JSMarshalAs] object instance) - { - C1 typed_instance = (C1)instance; - return typed_instance.P1; - } - public static C1 FromObject(object obj) - { - return obj switch + #nullable enable + // TypeShim generated TypeScript interop definitions + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + namespace N1; + public partial class C1Interop { - C1 instance => instance, - _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), - }; - } -} + [JSExport] + [return: JSMarshalAs] + public static object ctor([JSMarshalAs] object? p1) + { + MyClass? typed_p1 = p1 is { } p1Val ? MyClassInterop.FromObject(p1Val) : null; + return new C1(typed_p1); + } + [JSExport] + [return: JSMarshalAs] + public static int get_P1([JSMarshalAs] object instance) + { + C1 typed_instance = (C1)instance; + return typed_instance.P1; + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } + } -"""); + """); } [Test] @@ -507,7 +507,7 @@ public class C1 ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); // Mapping in ctor done with UserClassInterop.FromObject will not implement property initialization (validated by other tests) AssertEx.EqualOrDiff(interopClass, """ @@ -525,7 +525,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P1 = UserClassInterop.FromObject((jsObject.GetPropertyAsJSObject("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)))), + P1 = UserClassInterop.FromObject(jsObject.GetPropertyAsObjectNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), }; } [JSExport] @@ -533,7 +533,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) public static object get_P1([JSMarshalAs] object instance) { C1 typed_instance = (C1)instance; - return typed_instance.P1; + return (object)typed_instance.P1; } [JSExport] [return: JSMarshalAs] @@ -556,7 +556,7 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P1 = UserClassInterop.FromObject((jsObject.GetPropertyAsJSObject("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)))), + P1 = UserClassInterop.FromObject(jsObject.GetPropertyAsObjectNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), }; } } diff --git a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Delegates.cs b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Delegates.cs new file mode 100644 index 00000000..39515b3b --- /dev/null +++ b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Delegates.cs @@ -0,0 +1,1316 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System; +using System.Collections.Generic; +using System.Text; +using TypeShim.Generator.CSharp; +using TypeShim.Generator.Parsing; +using TypeShim.Shared; + +namespace TypeShim.Generator.Tests.CSharp; + +internal class CSharpInteropClassRendererTests_Delegates +{ + [Test] + public void CSharpInteropClass_MethodWithAction0ParameterType() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Action callback) => callback(); + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses.First(); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ +#nullable enable +// TypeShim generated TypeScript interop definitions +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +namespace N1; +public partial class C1Interop +{ + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs] + public static void M1([JSMarshalAs] object instance, [JSMarshalAs] Action callback) + { + C1 typed_instance = (C1)instance; + typed_instance.M1(callback); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } +} + +"""); + } + + [Test] + public void CSharpInteropClass_MethodWithAction1ParameterType() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Action callback) => callback(); + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses.First(); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ +#nullable enable +// TypeShim generated TypeScript interop definitions +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +namespace N1; +public partial class C1Interop +{ + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs] + public static void M1([JSMarshalAs] object instance, [JSMarshalAs>] Action callback) + { + C1 typed_instance = (C1)instance; + typed_instance.M1(callback); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } +} + +"""); + } + + [Test] + public void CSharpInteropClass_MethodWithAction2ParameterType() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Action callback) => callback(); + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses.First(); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ +#nullable enable +// TypeShim generated TypeScript interop definitions +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +namespace N1; +public partial class C1Interop +{ + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs] + public static void M1([JSMarshalAs] object instance, [JSMarshalAs>] Action callback) + { + C1 typed_instance = (C1)instance; + typed_instance.M1(callback); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } +} + +"""); + } + + [Test] + public void CSharpInteropClass_MethodWithAction3ParameterType() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Action callback) => callback(); + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses.First(); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ +#nullable enable +// TypeShim generated TypeScript interop definitions +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +namespace N1; +public partial class C1Interop +{ + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs] + public static void M1([JSMarshalAs] object instance, [JSMarshalAs>] Action callback) + { + C1 typed_instance = (C1)instance; + typed_instance.M1(callback); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } +} + +"""); + } + + [Test] + public void CSharpInteropClass_MethodWithFunc1ParameterType() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public string M1(Func callback) => callback(); + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses.First(); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ +#nullable enable +// TypeShim generated TypeScript interop definitions +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +namespace N1; +public partial class C1Interop +{ + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs] + public static string M1([JSMarshalAs] object instance, [JSMarshalAs>] Func callback) + { + C1 typed_instance = (C1)instance; + return typed_instance.M1(callback); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } +} + +"""); + } + + [Test] + public void CSharpInteropClass_MethodWithFunc2ParameterType() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public string M1(Func callback) => callback(1); + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses.First(); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ +#nullable enable +// TypeShim generated TypeScript interop definitions +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +namespace N1; +public partial class C1Interop +{ + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs] + public static string M1([JSMarshalAs] object instance, [JSMarshalAs>] Func callback) + { + C1 typed_instance = (C1)instance; + return typed_instance.M1(callback); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } +} + +"""); + } + + [Test] + public void CSharpInteropClass_MethodWithFunc3ParameterType() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public string M1(Func callback) => callback(1, true); + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses.First(); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ +#nullable enable +// TypeShim generated TypeScript interop definitions +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +namespace N1; +public partial class C1Interop +{ + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs] + public static string M1([JSMarshalAs] object instance, [JSMarshalAs>] Func callback) + { + C1 typed_instance = (C1)instance; + return typed_instance.M1(callback); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } +} + +"""); + } + + [Test] + public void CSharpInteropClass_Method_ActionUserClassParameterType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class MyClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Action callback) => callback(new MyClass() { Id = 1 }); + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses.First(), typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ +#nullable enable +// TypeShim generated TypeScript interop definitions +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +namespace N1; +public partial class C1Interop +{ + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs] + public static void M1([JSMarshalAs] object instance, [JSMarshalAs>] Action callback) + { + C1 typed_instance = (C1)instance; + Action typed_callback = (MyClass arg0) => callback(arg0); + typed_instance.M1(typed_callback); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } +} + +"""); + } + + [Test] + public void CSharpInteropClass_Method_ActionUserClassReturnType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class MyClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Action M1() => (MyClass obj) => {}; + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses.First(), typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ +#nullable enable +// TypeShim generated TypeScript interop definitions +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +namespace N1; +public partial class C1Interop +{ + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs>] + public static Action M1([JSMarshalAs] object instance) + { + C1 typed_instance = (C1)instance; + Action retVal = typed_instance.M1(); + return (object arg0) => retVal(MyClassInterop.FromObject(arg0)); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } +} + +"""); + } + + [Test] + public void CSharpInteropClass_Method_FunctionUserClassUserClassReturnType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class MyClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func M1() => (MyClass obj) => obj; + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses.First(), typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ +#nullable enable +// TypeShim generated TypeScript interop definitions +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +namespace N1; +public partial class C1Interop +{ + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs>] + public static Func M1([JSMarshalAs] object instance) + { + C1 typed_instance = (C1)instance; + Func retVal = typed_instance.M1(); + return (object arg0) => (object)retVal(MyClassInterop.FromObject(arg0)); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } +} + +"""); + } + + [Test] + public void CSharpInteropClass_MethodReturnType_FunctionNullableUserClassParamType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class MyClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func M1() {} + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses.First(), typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ +#nullable enable +// TypeShim generated TypeScript interop definitions +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +namespace N1; +public partial class C1Interop +{ + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs>] + public static Func M1([JSMarshalAs] object instance) + { + C1 typed_instance = (C1)instance; + Func retVal = typed_instance.M1(); + return (int arg0) => (object?)retVal(arg0); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } +} + +"""); + } + + [Test] + public void CSharpInteropClass_MethodReturnType_FunctionNullableUserClassReturnType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class MyClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func M1() {} + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses.First(), typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ +#nullable enable +// TypeShim generated TypeScript interop definitions +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +namespace N1; +public partial class C1Interop +{ + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs>] + public static Func M1([JSMarshalAs] object instance) + { + C1 typed_instance = (C1)instance; + Func retVal = typed_instance.M1(); + return () => (object?)retVal(); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } +} + +"""); + } + + [Test] + public void CSharpInteropClass_MethodParameterType_FunctionNullableUserClassParamType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class MyClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Func func) {} + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses.First(), typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ + #nullable enable + // TypeShim generated TypeScript interop definitions + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + namespace N1; + public partial class C1Interop + { + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs] + public static void M1([JSMarshalAs] object instance, [JSMarshalAs>] Func func) + { + C1 typed_instance = (C1)instance; + Func typed_func = (int arg0) => func(arg0) is { } funcVal ? MyClassInterop.FromObject(funcVal) : null; + typed_instance.M1(typed_func); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } + } + + """); + } + + [Test] + public void CSharpInteropClass_MethodParameterType_FunctionNullableUserClassReturnType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class MyClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Func func) {} + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses.First(), typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ + #nullable enable + // TypeShim generated TypeScript interop definitions + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + namespace N1; + public partial class C1Interop + { + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs] + public static void M1([JSMarshalAs] object instance, [JSMarshalAs>] Func func) + { + C1 typed_instance = (C1)instance; + Func typed_func = () => func() is { } funcVal ? MyClassInterop.FromObject(funcVal) : null; + typed_instance.M1(typed_func); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } + } + + """); + } + + [Test] + public void CSharpInteropClass_Method_FunctionWithUserClassReturnType_ShouldRenderConversion() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class MyClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public MyClass M1(Func func, Func paramFunc) + { + return func(paramFunc()); + } + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses.First(), typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ +#nullable enable +// TypeShim generated TypeScript interop definitions +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +namespace N1; +public partial class C1Interop +{ + [JSExport] + [return: JSMarshalAs] + public static object ctor() + { + return new C1(); + } + [JSExport] + [return: JSMarshalAs] + public static object M1([JSMarshalAs] object instance, [JSMarshalAs>] Func func, [JSMarshalAs>] Func paramFunc) + { + C1 typed_instance = (C1)instance; + Func typed_func = (MyClass arg0) => MyClassInterop.FromObject(func(arg0)); + Func typed_paramFunc = () => MyClassInterop.FromObject(paramFunc()); + return (object)typed_instance.M1(typed_func, typed_paramFunc); + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } +} + +"""); + } + + [Test] + public void CSharpInteropClass_PropertyType_FunctionUserClassUserClass() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class MyClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func P1 { get; set; } + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses.First(), typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ + #nullable enable + // TypeShim generated TypeScript interop definitions + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + namespace N1; + public partial class C1Interop + { + [JSExport] + [return: JSMarshalAs] + public static object ctor([JSMarshalAs] JSObject jsObject) + { + Func tmpP1 = jsObject.GetPropertyAsObjectObjectFunctionNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)); + return new C1() + { + P1 = (MyClass arg0) => MyClassInterop.FromObject(tmpP1(arg0)), + }; + } + [JSExport] + [return: JSMarshalAs>] + public static Func get_P1([JSMarshalAs] object instance) + { + C1 typed_instance = (C1)instance; + Func retVal = typed_instance.P1; + return (object arg0) => (object)retVal(MyClassInterop.FromObject(arg0)); + } + [JSExport] + [return: JSMarshalAs] + public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs>] Func value) + { + C1 typed_instance = (C1)instance; + Func typed_value = (MyClass arg0) => MyClassInterop.FromObject(value(arg0)); + typed_instance.P1 = typed_value; + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + JSObject jsObj => FromJSObject(jsObj), + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } + public static C1 FromJSObject(JSObject jsObject) + { + Func tmpP1 = jsObject.GetPropertyAsObjectObjectFunctionNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)); + return new C1() + { + P1 = (MyClass arg0) => MyClassInterop.FromObject(tmpP1(arg0)), + }; + } + } + + """); + } + + [Test] + public void CSharpInteropClass_PropertyType_FunctionUserClassUserClass_Unexported() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + //[TSExport] unexported + public class MyClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func P1 { get; set; } + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses.First(), typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ + #nullable enable + // TypeShim generated TypeScript interop definitions + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + namespace N1; + public partial class C1Interop + { + [JSExport] + [return: JSMarshalAs] + public static object ctor([JSMarshalAs] JSObject jsObject) + { + Func tmpP1 = jsObject.GetPropertyAsObjectObjectFunctionNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)); + return new C1() + { + P1 = (MyClass arg0) => (MyClass)tmpP1(arg0), + }; + } + [JSExport] + [return: JSMarshalAs>] + public static Func get_P1([JSMarshalAs] object instance) + { + C1 typed_instance = (C1)instance; + Func retVal = typed_instance.P1; + return (object arg0) => (object)retVal((MyClass)arg0); + } + [JSExport] + [return: JSMarshalAs] + public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs>] Func value) + { + C1 typed_instance = (C1)instance; + Func typed_value = (MyClass arg0) => (MyClass)value(arg0); + typed_instance.P1 = typed_value; + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + JSObject jsObj => FromJSObject(jsObj), + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } + public static C1 FromJSObject(JSObject jsObject) + { + Func tmpP1 = jsObject.GetPropertyAsObjectObjectFunctionNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)); + return new C1() + { + P1 = (MyClass arg0) => (MyClass)tmpP1(arg0), + }; + } + } + + """); + } + + [Test] + public void CSharpInteropClass_PropertyType_Action() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Action P1 { get; set; } + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses.First(); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ +#nullable enable +// TypeShim generated TypeScript interop definitions +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +namespace N1; +public partial class C1Interop +{ + [JSExport] + [return: JSMarshalAs] + public static object ctor([JSMarshalAs] JSObject jsObject) + { + return new C1() + { + P1 = jsObject.GetPropertyAsVoidActionNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), + }; + } + [JSExport] + [return: JSMarshalAs] + public static Action get_P1([JSMarshalAs] object instance) + { + C1 typed_instance = (C1)instance; + return typed_instance.P1; + } + [JSExport] + [return: JSMarshalAs] + public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs] Action value) + { + C1 typed_instance = (C1)instance; + typed_instance.P1 = value; + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + JSObject jsObj => FromJSObject(jsObj), + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } + public static C1 FromJSObject(JSObject jsObject) + { + return new C1() + { + P1 = jsObject.GetPropertyAsVoidActionNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), + }; + } +} + +"""); + } + + [Test] + public void CSharpInteropClass_PropertyType_ActionChar() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Action P1 { get; set; } + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses.First(); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); + + AssertEx.EqualOrDiff(interopClass, """ + #nullable enable + // TypeShim generated TypeScript interop definitions + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + namespace N1; + public partial class C1Interop + { + [JSExport] + [return: JSMarshalAs] + public static object ctor([JSMarshalAs] JSObject jsObject) + { + return new C1() + { + P1 = jsObject.GetPropertyAsCharVoidActionNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), + }; + } + [JSExport] + [return: JSMarshalAs>] + public static Action get_P1([JSMarshalAs] object instance) + { + C1 typed_instance = (C1)instance; + return typed_instance.P1; + } + [JSExport] + [return: JSMarshalAs] + public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs>] Action value) + { + C1 typed_instance = (C1)instance; + typed_instance.P1 = value; + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + JSObject jsObj => FromJSObject(jsObj), + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } + public static C1 FromJSObject(JSObject jsObject) + { + return new C1() + { + P1 = jsObject.GetPropertyAsCharVoidActionNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), + }; + } + } + + """); + } +} diff --git a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Properties.cs b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Properties.cs index e510011c..26045bfb 100644 --- a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Properties.cs +++ b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Properties.cs @@ -44,7 +44,7 @@ public class C1 ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -61,7 +61,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P1 = MyClassInterop.FromObject((jsObject.GetPropertyAsJSObject("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)))), + P1 = MyClassInterop.FromObject(jsObject.GetPropertyAsObjectNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), }; } [JSExport] @@ -69,7 +69,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) public static object get_P1([JSMarshalAs] object instance) { C1 typed_instance = (C1)instance; - return typed_instance.P1; + return (object)typed_instance.P1; } [JSExport] [return: JSMarshalAs] @@ -92,7 +92,7 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P1 = MyClassInterop.FromObject((jsObject.GetPropertyAsJSObject("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)))), + P1 = MyClassInterop.FromObject(jsObject.GetPropertyAsObjectNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), }; } } @@ -135,7 +135,7 @@ public static class C1 ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -150,7 +150,7 @@ public partial class C1Interop [return: JSMarshalAs] public static object get_P1() { - return C1.P1; + return (object)C1.P1; } [JSExport] [return: JSMarshalAs] @@ -199,62 +199,60 @@ public class C1 ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ -#nullable enable -// TypeShim generated TypeScript interop definitions -using System; -using System.Runtime.InteropServices.JavaScript; -using System.Threading.Tasks; -namespace N1; -public partial class C1Interop -{ - [JSExport] - [return: JSMarshalAs] - public static object ctor([JSMarshalAs] JSObject jsObject) - { - var P1Tmp = jsObject.GetPropertyAsJSObject("P1"); - return new C1() - { - P1 = P1Tmp != null ? MyClassInterop.FromObject(P1Tmp) : null, - }; - } - [JSExport] - [return: JSMarshalAs] - public static object? get_P1([JSMarshalAs] object instance) - { - C1 typed_instance = (C1)instance; - return typed_instance.P1; - } - [JSExport] - [return: JSMarshalAs] - public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs] object? value) - { - C1 typed_instance = (C1)instance; - MyClass? typed_value = value != null ? MyClassInterop.FromObject(value) : null; - typed_instance.P1 = typed_value; - } - public static C1 FromObject(object obj) - { - return obj switch + #nullable enable + // TypeShim generated TypeScript interop definitions + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + namespace N1; + public partial class C1Interop { - C1 instance => instance, - JSObject jsObj => FromJSObject(jsObj), - _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), - }; - } - public static C1 FromJSObject(JSObject jsObject) - { - var P1Tmp = jsObject.GetPropertyAsJSObject("P1"); - return new C1() - { - P1 = P1Tmp != null ? MyClassInterop.FromObject(P1Tmp) : null, - }; - } -} - -"""); + [JSExport] + [return: JSMarshalAs] + public static object ctor([JSMarshalAs] JSObject jsObject) + { + return new C1() + { + P1 = jsObject.GetPropertyAsObjectNullable("P1") is { } P1Val ? MyClassInterop.FromObject(P1Val) : null, + }; + } + [JSExport] + [return: JSMarshalAs] + public static object? get_P1([JSMarshalAs] object instance) + { + C1 typed_instance = (C1)instance; + return (object?)typed_instance.P1; + } + [JSExport] + [return: JSMarshalAs] + public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs] object? value) + { + C1 typed_instance = (C1)instance; + MyClass? typed_value = value is { } valueVal ? MyClassInterop.FromObject(valueVal) : null; + typed_instance.P1 = typed_value; + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + JSObject jsObj => FromJSObject(jsObj), + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } + public static C1 FromJSObject(JSObject jsObject) + { + return new C1() + { + P1 = jsObject.GetPropertyAsObjectNullable("P1") is { } P1Val ? MyClassInterop.FromObject(P1Val) : null, + }; + } + } + + """); } [Test] @@ -278,7 +276,7 @@ public class C1 InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -295,7 +293,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P1 = (jsObject.GetPropertyAsObject("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.GetPropertyAsObjectNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } [JSExport] @@ -325,7 +323,7 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P1 = (jsObject.GetPropertyAsObject("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.GetPropertyAsObjectNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } } @@ -354,7 +352,7 @@ public class C1 InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -371,7 +369,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P1 = (jsObject.GetPropertyAsInt32Array("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.GetPropertyAsInt32ArrayNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } [JSExport] @@ -401,7 +399,7 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P1 = (jsObject.GetPropertyAsInt32Array("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.GetPropertyAsInt32ArrayNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } } @@ -444,7 +442,7 @@ public class C1 ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -461,7 +459,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P1 = Array.ConvertAll((jsObject.GetPropertyAsJSObjectArray("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), e => MyClassInterop.FromObject(e)), + P1 = Array.ConvertAll(jsObject.GetPropertyAsObjectArrayNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), e => MyClassInterop.FromObject(e)), }; } [JSExport] @@ -469,7 +467,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) public static object[] get_P1([JSMarshalAs] object instance) { C1 typed_instance = (C1)instance; - return typed_instance.P1; + return (object[])typed_instance.P1; } [JSExport] [return: JSMarshalAs] @@ -492,7 +490,7 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P1 = Array.ConvertAll((jsObject.GetPropertyAsJSObjectArray("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), e => MyClassInterop.FromObject(e)), + P1 = Array.ConvertAll(jsObject.GetPropertyAsObjectArrayNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), e => MyClassInterop.FromObject(e)), }; } } @@ -535,60 +533,60 @@ public class C1 ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ -#nullable enable -// TypeShim generated TypeScript interop definitions -using System; -using System.Runtime.InteropServices.JavaScript; -using System.Threading.Tasks; -namespace N1; -public partial class C1Interop -{ - [JSExport] - [return: JSMarshalAs] - public static object ctor([JSMarshalAs] JSObject jsObject) - { - return new C1() + #nullable enable + // TypeShim generated TypeScript interop definitions + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + namespace N1; + public partial class C1Interop { - P1 = Array.ConvertAll((jsObject.GetPropertyAsJSObjectArray("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), e => e != null ? MyClassInterop.FromObject(e) : null), - }; - } - [JSExport] - [return: JSMarshalAs>] - public static object?[] get_P1([JSMarshalAs] object instance) - { - C1 typed_instance = (C1)instance; - return typed_instance.P1; - } - [JSExport] - [return: JSMarshalAs] - public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs>] object?[] value) - { - C1 typed_instance = (C1)instance; - MyClass?[] typed_value = Array.ConvertAll(value, e => e != null ? MyClassInterop.FromObject(e) : null); - typed_instance.P1 = typed_value; - } - public static C1 FromObject(object obj) - { - return obj switch - { - C1 instance => instance, - JSObject jsObj => FromJSObject(jsObj), - _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), - }; - } - public static C1 FromJSObject(JSObject jsObject) - { - return new C1() - { - P1 = Array.ConvertAll((jsObject.GetPropertyAsJSObjectArray("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), e => e != null ? MyClassInterop.FromObject(e) : null), - }; - } -} + [JSExport] + [return: JSMarshalAs] + public static object ctor([JSMarshalAs] JSObject jsObject) + { + return new C1() + { + P1 = Array.ConvertAll(jsObject.GetPropertyAsObjectNullableArrayNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), e => e is { } eVal ? MyClassInterop.FromObject(eVal) : null), + }; + } + [JSExport] + [return: JSMarshalAs>] + public static object?[] get_P1([JSMarshalAs] object instance) + { + C1 typed_instance = (C1)instance; + return (object?[])typed_instance.P1; + } + [JSExport] + [return: JSMarshalAs] + public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs>] object?[] value) + { + C1 typed_instance = (C1)instance; + MyClass?[] typed_value = Array.ConvertAll(value, e => e is { } eVal ? MyClassInterop.FromObject(eVal) : null); + typed_instance.P1 = typed_value; + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + JSObject jsObj => FromJSObject(jsObj), + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } + public static C1 FromJSObject(JSObject jsObject) + { + return new C1() + { + P1 = Array.ConvertAll(jsObject.GetPropertyAsObjectNullableArrayNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), e => e is { } eVal ? MyClassInterop.FromObject(eVal) : null), + }; + } + } -"""); + """); } [Test] @@ -625,62 +623,60 @@ public class C1 ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ -#nullable enable -// TypeShim generated TypeScript interop definitions -using System; -using System.Runtime.InteropServices.JavaScript; -using System.Threading.Tasks; -namespace N1; -public partial class C1Interop -{ - [JSExport] - [return: JSMarshalAs] - public static object ctor([JSMarshalAs] JSObject jsObject) - { - var P1Tmp = jsObject.GetPropertyAsJSObjectArray("P1"); - return new C1() + #nullable enable + // TypeShim generated TypeScript interop definitions + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + namespace N1; + public partial class C1Interop { - P1 = P1Tmp != null ? Array.ConvertAll(P1Tmp, e => MyClassInterop.FromObject(e)) : null, - }; - } - [JSExport] - [return: JSMarshalAs>] - public static object[]? get_P1([JSMarshalAs] object instance) - { - C1 typed_instance = (C1)instance; - return typed_instance.P1; - } - [JSExport] - [return: JSMarshalAs] - public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs>] object[]? value) - { - C1 typed_instance = (C1)instance; - MyClass[]? typed_value = value != null ? Array.ConvertAll(value, e => MyClassInterop.FromObject(e)) : null; - typed_instance.P1 = typed_value; - } - public static C1 FromObject(object obj) - { - return obj switch - { - C1 instance => instance, - JSObject jsObj => FromJSObject(jsObj), - _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), - }; - } - public static C1 FromJSObject(JSObject jsObject) - { - var P1Tmp = jsObject.GetPropertyAsJSObjectArray("P1"); - return new C1() - { - P1 = P1Tmp != null ? Array.ConvertAll(P1Tmp, e => MyClassInterop.FromObject(e)) : null, - }; - } -} + [JSExport] + [return: JSMarshalAs] + public static object ctor([JSMarshalAs] JSObject jsObject) + { + return new C1() + { + P1 = jsObject.GetPropertyAsObjectArrayNullable("P1") is { } P1Val ? Array.ConvertAll(P1Val, e => MyClassInterop.FromObject(e)) : null, + }; + } + [JSExport] + [return: JSMarshalAs>] + public static object[]? get_P1([JSMarshalAs] object instance) + { + C1 typed_instance = (C1)instance; + return (object[]?)typed_instance.P1; + } + [JSExport] + [return: JSMarshalAs] + public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs>] object[]? value) + { + C1 typed_instance = (C1)instance; + MyClass[]? typed_value = value is { } valueVal ? Array.ConvertAll(valueVal, e => MyClassInterop.FromObject(e)) : null; + typed_instance.P1 = typed_value; + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + JSObject jsObj => FromJSObject(jsObj), + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } + public static C1 FromJSObject(JSObject jsObject) + { + return new C1() + { + P1 = jsObject.GetPropertyAsObjectArrayNullable("P1") is { } P1Val ? Array.ConvertAll(P1Val, e => MyClassInterop.FromObject(e)) : null, + }; + } + } -"""); + """); } [Test] @@ -718,7 +714,7 @@ public class C1 ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -733,10 +729,9 @@ public partial class C1Interop [return: JSMarshalAs] public static object ctor([JSMarshalAs] JSObject jsObject) { - var P1Tmp = jsObject.GetPropertyAsJSObjectArray("P1"); return new C1() { - P1 = P1Tmp != null ? Array.ConvertAll(P1Tmp, e => e != null ? MyClassInterop.FromObject(e) : null) : null, + P1 = jsObject.GetPropertyAsObjectNullableArrayNullable("P1") is { } P1Val ? Array.ConvertAll(P1Val, e => e is { } eVal ? MyClassInterop.FromObject(eVal) : null) : null, }; } [JSExport] @@ -744,14 +739,14 @@ public static object ctor([JSMarshalAs] JSObject jsObject) public static object?[]? get_P1([JSMarshalAs] object instance) { C1 typed_instance = (C1)instance; - return typed_instance.P1; + return (object?[]?)typed_instance.P1; } [JSExport] [return: JSMarshalAs] public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs>] object?[]? value) { C1 typed_instance = (C1)instance; - MyClass?[]? typed_value = value != null ? Array.ConvertAll(value, e => e != null ? MyClassInterop.FromObject(e) : null) : null; + MyClass?[]? typed_value = value is { } valueVal ? Array.ConvertAll(valueVal, e => e is { } eVal ? MyClassInterop.FromObject(eVal) : null) : null; typed_instance.P1 = typed_value; } public static C1 FromObject(object obj) @@ -765,10 +760,9 @@ public static C1 FromObject(object obj) } public static C1 FromJSObject(JSObject jsObject) { - var P1Tmp = jsObject.GetPropertyAsJSObjectArray("P1"); return new C1() { - P1 = P1Tmp != null ? Array.ConvertAll(P1Tmp, e => e != null ? MyClassInterop.FromObject(e) : null) : null, + P1 = jsObject.GetPropertyAsObjectNullableArrayNullable("P1") is { } P1Val ? Array.ConvertAll(P1Val, e => e is { } eVal ? MyClassInterop.FromObject(eVal) : null) : null, }; } } @@ -811,7 +805,7 @@ public class C1 ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -826,9 +820,8 @@ public partial class C1Interop [return: JSMarshalAs] public static object ctor([JSMarshalAs] JSObject jsObject) { - var P1Tmp = jsObject.GetPropertyAsJSObjectTask("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)); TaskCompletionSource P1Tcs = new(); - P1Tmp.ContinueWith(t => { + (jsObject.GetPropertyAsObjectTaskNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))).ContinueWith(t => { if (t.IsFaulted) P1Tcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) P1Tcs.SetCanceled(); else P1Tcs.SetResult(MyClassInterop.FromObject(t.Result)); @@ -844,7 +837,7 @@ public static Task get_P1([JSMarshalAs] object instance) { C1 typed_instance = (C1)instance; TaskCompletionSource retValTcs = new(); - typed_instance.P1.ContinueWith(t => { + (typed_instance.P1).ContinueWith(t => { if (t.IsFaulted) retValTcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) retValTcs.SetCanceled(); else retValTcs.SetResult((object)t.Result); @@ -857,7 +850,7 @@ public static void set_P1([JSMarshalAs] object instance, [JSMarshalA { C1 typed_instance = (C1)instance; TaskCompletionSource valueTcs = new(); - value.ContinueWith(t => { + (value).ContinueWith(t => { if (t.IsFaulted) valueTcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) valueTcs.SetCanceled(); else valueTcs.SetResult(MyClassInterop.FromObject(t.Result)); @@ -876,9 +869,8 @@ public static C1 FromObject(object obj) } public static C1 FromJSObject(JSObject jsObject) { - var P1Tmp = jsObject.GetPropertyAsJSObjectTask("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)); TaskCompletionSource P1Tcs = new(); - P1Tmp.ContinueWith(t => { + (jsObject.GetPropertyAsObjectTaskNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))).ContinueWith(t => { if (t.IsFaulted) P1Tcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) P1Tcs.SetCanceled(); else P1Tcs.SetResult(MyClassInterop.FromObject(t.Result)); @@ -928,86 +920,84 @@ public class C1 ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ -#nullable enable -// TypeShim generated TypeScript interop definitions -using System; -using System.Runtime.InteropServices.JavaScript; -using System.Threading.Tasks; -namespace N1; -public partial class C1Interop -{ - [JSExport] - [return: JSMarshalAs] - public static object ctor([JSMarshalAs] JSObject jsObject) - { - var P1Tmp = jsObject.GetPropertyAsJSObjectTask("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)); - TaskCompletionSource P1Tcs = new(); - P1Tmp.ContinueWith(t => { - if (t.IsFaulted) P1Tcs.SetException(t.Exception.InnerExceptions); - else if (t.IsCanceled) P1Tcs.SetCanceled(); - else P1Tcs.SetResult(t.Result != null ? MyClassInterop.FromObject(t.Result) : null); - }, TaskContinuationOptions.ExecuteSynchronously); - return new C1() - { - P1 = P1Tcs.Task, - }; - } - [JSExport] - [return: JSMarshalAs>] - public static Task get_P1([JSMarshalAs] object instance) - { - C1 typed_instance = (C1)instance; - TaskCompletionSource retValTcs = new(); - typed_instance.P1.ContinueWith(t => { - if (t.IsFaulted) retValTcs.SetException(t.Exception.InnerExceptions); - else if (t.IsCanceled) retValTcs.SetCanceled(); - else retValTcs.SetResult(t.Result != null ? (object)t.Result : null); - }, TaskContinuationOptions.ExecuteSynchronously); - return retValTcs.Task; - } - [JSExport] - [return: JSMarshalAs] - public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs>] Task value) - { - C1 typed_instance = (C1)instance; - TaskCompletionSource valueTcs = new(); - value.ContinueWith(t => { - if (t.IsFaulted) valueTcs.SetException(t.Exception.InnerExceptions); - else if (t.IsCanceled) valueTcs.SetCanceled(); - else valueTcs.SetResult(t.Result != null ? MyClassInterop.FromObject(t.Result) : null); - }, TaskContinuationOptions.ExecuteSynchronously); - Task typed_value = valueTcs.Task; - typed_instance.P1 = typed_value; - } - public static C1 FromObject(object obj) - { - return obj switch + #nullable enable + // TypeShim generated TypeScript interop definitions + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + namespace N1; + public partial class C1Interop { - C1 instance => instance, - JSObject jsObj => FromJSObject(jsObj), - _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), - }; - } - public static C1 FromJSObject(JSObject jsObject) - { - var P1Tmp = jsObject.GetPropertyAsJSObjectTask("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)); - TaskCompletionSource P1Tcs = new(); - P1Tmp.ContinueWith(t => { - if (t.IsFaulted) P1Tcs.SetException(t.Exception.InnerExceptions); - else if (t.IsCanceled) P1Tcs.SetCanceled(); - else P1Tcs.SetResult(t.Result != null ? MyClassInterop.FromObject(t.Result) : null); - }, TaskContinuationOptions.ExecuteSynchronously); - return new C1() - { - P1 = P1Tcs.Task, - }; - } -} + [JSExport] + [return: JSMarshalAs] + public static object ctor([JSMarshalAs] JSObject jsObject) + { + TaskCompletionSource P1Tcs = new(); + (jsObject.GetPropertyAsObjectNullableTaskNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))).ContinueWith(t => { + if (t.IsFaulted) P1Tcs.SetException(t.Exception.InnerExceptions); + else if (t.IsCanceled) P1Tcs.SetCanceled(); + else P1Tcs.SetResult(t.Result is { } P1TcsVal ? MyClassInterop.FromObject(P1TcsVal) : null); + }, TaskContinuationOptions.ExecuteSynchronously); + return new C1() + { + P1 = P1Tcs.Task, + }; + } + [JSExport] + [return: JSMarshalAs>] + public static Task get_P1([JSMarshalAs] object instance) + { + C1 typed_instance = (C1)instance; + TaskCompletionSource retValTcs = new(); + (typed_instance.P1).ContinueWith(t => { + if (t.IsFaulted) retValTcs.SetException(t.Exception.InnerExceptions); + else if (t.IsCanceled) retValTcs.SetCanceled(); + else retValTcs.SetResult(t.Result is { } retValTcsVal ? (object)retValTcsVal : null); + }, TaskContinuationOptions.ExecuteSynchronously); + return retValTcs.Task; + } + [JSExport] + [return: JSMarshalAs] + public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs>] Task value) + { + C1 typed_instance = (C1)instance; + TaskCompletionSource valueTcs = new(); + (value).ContinueWith(t => { + if (t.IsFaulted) valueTcs.SetException(t.Exception.InnerExceptions); + else if (t.IsCanceled) valueTcs.SetCanceled(); + else valueTcs.SetResult(t.Result is { } valueTcsVal ? MyClassInterop.FromObject(valueTcsVal) : null); + }, TaskContinuationOptions.ExecuteSynchronously); + Task typed_value = valueTcs.Task; + typed_instance.P1 = typed_value; + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + JSObject jsObj => FromJSObject(jsObj), + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } + public static C1 FromJSObject(JSObject jsObject) + { + TaskCompletionSource P1Tcs = new(); + (jsObject.GetPropertyAsObjectNullableTaskNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))).ContinueWith(t => { + if (t.IsFaulted) P1Tcs.SetException(t.Exception.InnerExceptions); + else if (t.IsCanceled) P1Tcs.SetCanceled(); + else P1Tcs.SetResult(t.Result is { } P1TcsVal ? MyClassInterop.FromObject(P1TcsVal) : null); + }, TaskContinuationOptions.ExecuteSynchronously); + return new C1() + { + P1 = P1Tcs.Task, + }; + } + } -"""); + """); } [Test] @@ -1045,86 +1035,84 @@ public class C1 ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); - AssertEx.EqualOrDiff(interopClass, """ -#nullable enable -// TypeShim generated TypeScript interop definitions -using System; -using System.Runtime.InteropServices.JavaScript; -using System.Threading.Tasks; -namespace N1; -public partial class C1Interop -{ - [JSExport] - [return: JSMarshalAs] - public static object ctor([JSMarshalAs] JSObject jsObject) - { - var P1Tmp = jsObject.GetPropertyAsJSObjectTask("P1"); - TaskCompletionSource? P1Tcs = P1Tmp != null ? new() : null; - P1Tmp?.ContinueWith(t => { - if (t.IsFaulted) P1Tcs!.SetException(t.Exception.InnerExceptions); - else if (t.IsCanceled) P1Tcs!.SetCanceled(); - else P1Tcs!.SetResult(t.Result != null ? MyClassInterop.FromObject(t.Result) : null); - }, TaskContinuationOptions.ExecuteSynchronously); - return new C1() - { - P1 = P1Tcs?.Task, - }; - } - [JSExport] - [return: JSMarshalAs>] - public static Task? get_P1([JSMarshalAs] object instance) - { - C1 typed_instance = (C1)instance; - TaskCompletionSource? retValTcs = typed_instance.P1 != null ? new() : null; - typed_instance.P1?.ContinueWith(t => { - if (t.IsFaulted) retValTcs!.SetException(t.Exception.InnerExceptions); - else if (t.IsCanceled) retValTcs!.SetCanceled(); - else retValTcs!.SetResult(t.Result != null ? (object)t.Result : null); - }, TaskContinuationOptions.ExecuteSynchronously); - return retValTcs?.Task; - } - [JSExport] - [return: JSMarshalAs] - public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs>] Task? value) - { - C1 typed_instance = (C1)instance; - TaskCompletionSource? valueTcs = value != null ? new() : null; - value?.ContinueWith(t => { - if (t.IsFaulted) valueTcs!.SetException(t.Exception.InnerExceptions); - else if (t.IsCanceled) valueTcs!.SetCanceled(); - else valueTcs!.SetResult(t.Result != null ? MyClassInterop.FromObject(t.Result) : null); - }, TaskContinuationOptions.ExecuteSynchronously); - Task? typed_value = valueTcs?.Task; - typed_instance.P1 = typed_value; - } - public static C1 FromObject(object obj) - { - return obj switch - { - C1 instance => instance, - JSObject jsObj => FromJSObject(jsObj), - _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), - }; - } - public static C1 FromJSObject(JSObject jsObject) - { - var P1Tmp = jsObject.GetPropertyAsJSObjectTask("P1"); - TaskCompletionSource? P1Tcs = P1Tmp != null ? new() : null; - P1Tmp?.ContinueWith(t => { - if (t.IsFaulted) P1Tcs!.SetException(t.Exception.InnerExceptions); - else if (t.IsCanceled) P1Tcs!.SetCanceled(); - else P1Tcs!.SetResult(t.Result != null ? MyClassInterop.FromObject(t.Result) : null); - }, TaskContinuationOptions.ExecuteSynchronously); - return new C1() + AssertEx.EqualOrDiff(interopClass, """ + #nullable enable + // TypeShim generated TypeScript interop definitions + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + namespace N1; + public partial class C1Interop { - P1 = P1Tcs?.Task, - }; - } -} + [JSExport] + [return: JSMarshalAs] + public static object ctor([JSMarshalAs] JSObject jsObject) + { + TaskCompletionSource? P1Tcs = jsObject.GetPropertyAsObjectNullableTaskNullable("P1") != null ? new() : null; + jsObject.GetPropertyAsObjectNullableTaskNullable("P1")?.ContinueWith(t => { + if (t.IsFaulted) P1Tcs!.SetException(t.Exception.InnerExceptions); + else if (t.IsCanceled) P1Tcs!.SetCanceled(); + else P1Tcs!.SetResult(t.Result is { } P1TcsVal ? MyClassInterop.FromObject(P1TcsVal) : null); + }, TaskContinuationOptions.ExecuteSynchronously); + return new C1() + { + P1 = P1Tcs?.Task, + }; + } + [JSExport] + [return: JSMarshalAs>] + public static Task? get_P1([JSMarshalAs] object instance) + { + C1 typed_instance = (C1)instance; + TaskCompletionSource? retValTcs = typed_instance.P1 != null ? new() : null; + typed_instance.P1?.ContinueWith(t => { + if (t.IsFaulted) retValTcs!.SetException(t.Exception.InnerExceptions); + else if (t.IsCanceled) retValTcs!.SetCanceled(); + else retValTcs!.SetResult(t.Result is { } retValTcsVal ? (object)retValTcsVal : null); + }, TaskContinuationOptions.ExecuteSynchronously); + return retValTcs?.Task; + } + [JSExport] + [return: JSMarshalAs] + public static void set_P1([JSMarshalAs] object instance, [JSMarshalAs>] Task? value) + { + C1 typed_instance = (C1)instance; + TaskCompletionSource? valueTcs = value != null ? new() : null; + value?.ContinueWith(t => { + if (t.IsFaulted) valueTcs!.SetException(t.Exception.InnerExceptions); + else if (t.IsCanceled) valueTcs!.SetCanceled(); + else valueTcs!.SetResult(t.Result is { } valueTcsVal ? MyClassInterop.FromObject(valueTcsVal) : null); + }, TaskContinuationOptions.ExecuteSynchronously); + Task? typed_value = valueTcs?.Task; + typed_instance.P1 = typed_value; + } + public static C1 FromObject(object obj) + { + return obj switch + { + C1 instance => instance, + JSObject jsObj => FromJSObject(jsObj), + _ => throw new ArgumentException($"Invalid object type {obj?.GetType().ToString() ?? "null"}", nameof(obj)), + }; + } + public static C1 FromJSObject(JSObject jsObject) + { + TaskCompletionSource? P1Tcs = jsObject.GetPropertyAsObjectNullableTaskNullable("P1") != null ? new() : null; + jsObject.GetPropertyAsObjectNullableTaskNullable("P1")?.ContinueWith(t => { + if (t.IsFaulted) P1Tcs!.SetException(t.Exception.InnerExceptions); + else if (t.IsCanceled) P1Tcs!.SetCanceled(); + else P1Tcs!.SetResult(t.Result is { } P1TcsVal ? MyClassInterop.FromObject(P1TcsVal) : null); + }, TaskContinuationOptions.ExecuteSynchronously); + return new C1() + { + P1 = P1Tcs?.Task, + }; + } + } -"""); + """); } [TestCase("Version")] @@ -1149,7 +1137,7 @@ public class C1 InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -1166,7 +1154,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P1 = ({{typeName}}[])(jsObject.GetPropertyAsObjectArray("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = ({{typeName}}[])jsObject.GetPropertyAsObjectArrayNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } [JSExport] @@ -1174,7 +1162,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) public static object[] get_P1([JSMarshalAs] object instance) { C1 typed_instance = (C1)instance; - return typed_instance.P1; + return (object[])typed_instance.P1; } [JSExport] [return: JSMarshalAs] @@ -1197,7 +1185,7 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P1 = ({{typeName}}[])(jsObject.GetPropertyAsObjectArray("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = ({{typeName}}[])jsObject.GetPropertyAsObjectArrayNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } } diff --git a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Snapshots.cs b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Snapshots.cs index fd1f47aa..44c97302 100644 --- a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Snapshots.cs +++ b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_Snapshots.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using System; using System.Collections.Generic; +using System.Runtime.InteropServices.JavaScript; using System.Text; using TypeShim.Generator.CSharp; using TypeShim.Generator.Parsing; @@ -11,7 +12,7 @@ namespace TypeShim.Generator.Tests.CSharp; internal class CSharpInteropClassRendererTests_Snapshots { - [TestCase("string", "string", "JSType.String", "GetPropertyAsString")] + [TestCase("string", "string", "JSType.String", "GetPropertyAsStringNullable")] [TestCase("double", "double", "JSType.Number", "GetPropertyAsDoubleNullable")] [TestCase("bool", "bool", "JSType.Boolean", "GetPropertyAsBooleanNullable")] public void CSharpInteropClass_SupportedPropertyType_GeneratesFromJSObjectMethod(string typeExpression, string interopTypeExpression, string jsType, string jsObjectMethod) @@ -35,7 +36,7 @@ public class C1 InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -52,7 +53,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P1 = (jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } [JSExport] @@ -82,7 +83,7 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P1 = (jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } } @@ -117,7 +118,7 @@ public class C1 InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -134,7 +135,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P2 = (List<{{typeExpression}}>)(jsObject.GetPropertyAsObject("P2") ?? throw new ArgumentException("Non-nullable property 'P2' missing or of invalid type", nameof(jsObject))), + P2 = (List<{{typeExpression}}>)jsObject.GetPropertyAsObjectNullable("P2") ?? throw new ArgumentException("Non-nullable property 'P2' missing or of invalid type", nameof(jsObject)), }; } [JSExport] @@ -142,7 +143,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) public static object get_P2([JSMarshalAs] object instance) { C1 typed_instance = (C1)instance; - return typed_instance.P2; + return (object)typed_instance.P2; } [JSExport] [return: JSMarshalAs] @@ -165,7 +166,7 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P2 = (List<{{typeExpression}}>)(jsObject.GetPropertyAsObject("P2") ?? throw new ArgumentException("Non-nullable property 'P2' missing or of invalid type", nameof(jsObject))), + P2 = (List<{{typeExpression}}>)jsObject.GetPropertyAsObjectNullable("P2") ?? throw new ArgumentException("Non-nullable property 'P2' missing or of invalid type", nameof(jsObject)), }; } } @@ -197,7 +198,7 @@ public class C1 InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -214,8 +215,8 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P1 = ({{typeName}}[])(jsObject.GetPropertyAsObjectArray("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), - P2 = (jsObject.GetPropertyAsInt32Nullable("P2") ?? throw new ArgumentException("Non-nullable property 'P2' missing or of invalid type", nameof(jsObject))), + P1 = ({{typeName}}[])jsObject.GetPropertyAsObjectArrayNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), + P2 = jsObject.GetPropertyAsInt32Nullable("P2") ?? throw new ArgumentException("Non-nullable property 'P2' missing or of invalid type", nameof(jsObject)), }; } [JSExport] @@ -223,7 +224,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) public static object[] get_P1([JSMarshalAs] object instance) { C1 typed_instance = (C1)instance; - return typed_instance.P1; + return (object[])typed_instance.P1; } [JSExport] [return: JSMarshalAs] @@ -260,8 +261,8 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P1 = ({{typeName}}[])(jsObject.GetPropertyAsObjectArray("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), - P2 = (jsObject.GetPropertyAsInt32Nullable("P2") ?? throw new ArgumentException("Non-nullable property 'P2' missing or of invalid type", nameof(jsObject))), + P1 = ({{typeName}}[])jsObject.GetPropertyAsObjectArrayNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), + P2 = jsObject.GetPropertyAsInt32Nullable("P2") ?? throw new ArgumentException("Non-nullable property 'P2' missing or of invalid type", nameof(jsObject)), }; } } @@ -292,9 +293,9 @@ public class C1 InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); - AssertEx.EqualOrDiff(interopClass, """ + AssertEx.EqualOrDiff(interopClass, """ #nullable enable // TypeShim generated TypeScript interop definitions using System; @@ -307,9 +308,8 @@ public partial class C1Interop [return: JSMarshalAs] public static object ctor([JSMarshalAs] JSObject jsObject) { - var P1Tmp = jsObject.GetPropertyAsObjectTask("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)); TaskCompletionSource<{{typeName}}> P1Tcs = new(); - P1Tmp.ContinueWith(t => { + (jsObject.GetPropertyAsObjectTaskNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))).ContinueWith(t => { if (t.IsFaulted) P1Tcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) P1Tcs.SetCanceled(); else P1Tcs.SetResult(({{typeName}})t.Result); @@ -317,7 +317,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) return new C1() { P1 = P1Tcs.Task, - P2 = (jsObject.GetPropertyAsInt32Nullable("P2") ?? throw new ArgumentException("Non-nullable property 'P2' missing or of invalid type", nameof(jsObject))), + P2 = jsObject.GetPropertyAsInt32Nullable("P2") ?? throw new ArgumentException("Non-nullable property 'P2' missing or of invalid type", nameof(jsObject)), }; } [JSExport] @@ -326,7 +326,7 @@ public static Task get_P1([JSMarshalAs] object instance) { C1 typed_instance = (C1)instance; TaskCompletionSource retValTcs = new(); - typed_instance.P1.ContinueWith(t => { + (typed_instance.P1).ContinueWith(t => { if (t.IsFaulted) retValTcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) retValTcs.SetCanceled(); else retValTcs.SetResult((object)t.Result); @@ -339,7 +339,7 @@ public static void set_P1([JSMarshalAs] object instance, [JSMarshalA { C1 typed_instance = (C1)instance; TaskCompletionSource<{{typeName}}> valueTcs = new(); - value.ContinueWith(t => { + (value).ContinueWith(t => { if (t.IsFaulted) valueTcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) valueTcs.SetCanceled(); else valueTcs.SetResult(({{typeName}})t.Result); @@ -372,9 +372,8 @@ public static C1 FromObject(object obj) } public static C1 FromJSObject(JSObject jsObject) { - var P1Tmp = jsObject.GetPropertyAsObjectTask("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)); TaskCompletionSource<{{typeName}}> P1Tcs = new(); - P1Tmp.ContinueWith(t => { + (jsObject.GetPropertyAsObjectTaskNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))).ContinueWith(t => { if (t.IsFaulted) P1Tcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) P1Tcs.SetCanceled(); else P1Tcs.SetResult(({{typeName}})t.Result); @@ -382,7 +381,7 @@ public static C1 FromJSObject(JSObject jsObject) return new C1() { P1 = P1Tcs.Task, - P2 = (jsObject.GetPropertyAsInt32Nullable("P2") ?? throw new ArgumentException("Non-nullable property 'P2' missing or of invalid type", nameof(jsObject))), + P2 = jsObject.GetPropertyAsInt32Nullable("P2") ?? throw new ArgumentException("Non-nullable property 'P2' missing or of invalid type", nameof(jsObject)), }; } } diff --git a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemArrayReturnType.cs b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemArrayReturnType.cs index 21af9867..041de96c 100644 --- a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemArrayReturnType.cs +++ b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemArrayReturnType.cs @@ -43,7 +43,7 @@ private C1() {} InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -107,14 +107,13 @@ public static MyClass[] M1() SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; Assert.That(exportedClasses, Has.Count.EqualTo(2)); - INamedTypeSymbol classSymbol = exportedClasses.First(); - InteropTypeInfoCache typeCache = new(); - ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); - RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses.ElementAt(0), typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.ElementAt(1), typeCache).Build(); + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); - Assert.That(interopClass, Is.EqualTo(""" + AssertEx.EqualOrDiff(interopClass, """ #nullable enable // TypeShim generated TypeScript interop definitions using System; @@ -127,7 +126,7 @@ public partial class C1Interop [return: JSMarshalAs>] public static object[] M1() { - return C1.M1(); + return (object[])C1.M1(); } public static C1 FromObject(object obj) { @@ -139,7 +138,7 @@ public static C1 FromObject(object obj) } } -""")); +"""); } [Test] @@ -181,7 +180,7 @@ public Task M1() InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -198,7 +197,7 @@ public static Task M1([JSMarshalAs] object instance) { C1 typed_instance = (C1)instance; TaskCompletionSource retValTcs = new(); - typed_instance.M1().ContinueWith(t => { + (typed_instance.M1()).ContinueWith(t => { if (t.IsFaulted) retValTcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) retValTcs.SetCanceled(); else retValTcs.SetResult((object)t.Result); @@ -244,7 +243,7 @@ private C1() {} InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -259,7 +258,7 @@ public partial class C1Interop [return: JSMarshalAs>] public static object[] M1() { - return C1.M1(); + return (object[])C1.M1(); } public static C1 FromObject(object obj) { diff --git a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemDateTimeParameterType.cs b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemDateTimeParameterType.cs index cd5cd2c8..28a638df 100644 --- a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemDateTimeParameterType.cs +++ b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemDateTimeParameterType.cs @@ -32,7 +32,7 @@ public static void M1({{typeName}} p1) InteropTypeInfoCache typeInfoCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeInfoCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -79,7 +79,7 @@ public void M1({{typeName}} p1) InteropTypeInfoCache typeInfoCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeInfoCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -132,7 +132,7 @@ public class C1 InteropTypeInfoCache typeInfoCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeInfoCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -149,7 +149,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P1 = (jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } [JSExport] @@ -179,7 +179,7 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P1 = (jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } } diff --git a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemDateTimeReturnType.cs b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemDateTimeReturnType.cs index f1e776fd..8a0c96a3 100644 --- a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemDateTimeReturnType.cs +++ b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemDateTimeReturnType.cs @@ -32,7 +32,7 @@ public static class C1 InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -79,7 +79,7 @@ private C1() {} InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable diff --git a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemNumericParameterType.cs b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemNumericParameterType.cs index fe7fc6ba..9081793e 100644 --- a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemNumericParameterType.cs +++ b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemNumericParameterType.cs @@ -45,7 +45,7 @@ public static void M1({{typeExpression}} arg1) InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -104,7 +104,7 @@ public static void M1({{typeExpression}} arg1) InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -164,7 +164,7 @@ public void M1({{typeExpression}} arg1) InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -203,8 +203,8 @@ public static C1 FromObject(object obj) [TestCase("int", "int", "GetPropertyAsInt32Nullable")] [TestCase("Int64", "long", "GetPropertyAsInt64Nullable")] [TestCase("long", "long", "GetPropertyAsInt64Nullable")] - [TestCase("Single", "float", "GetPropertyAsFloatNullable")] - [TestCase("float", "float", "GetPropertyAsFloatNullable")] + [TestCase("Single", "float", "GetPropertyAsSingleNullable")] + [TestCase("float", "float", "GetPropertyAsSingleNullable")] [TestCase("Double", "double", "GetPropertyAsDoubleNullable")] [TestCase("double", "double", "GetPropertyAsDoubleNullable")] [TestCase("IntPtr", "nint", "GetPropertyAsIntPtrNullable")] @@ -229,7 +229,7 @@ public class C1 InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -246,7 +246,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P1 = (jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } [JSExport] @@ -276,7 +276,7 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P1 = (jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } } diff --git a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemNumericReturnType.cs b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemNumericReturnType.cs index 60c7314b..ee8d29ec 100644 --- a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemNumericReturnType.cs +++ b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemNumericReturnType.cs @@ -45,7 +45,7 @@ public static class C1 InteropTypeInfoCache typeInfoCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeInfoCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -104,7 +104,7 @@ public static class C1 InteropTypeInfoCache typeInfoCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeInfoCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable diff --git a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemStringParameterType.cs b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemStringParameterType.cs index d5a9f490..0d0d06da 100644 --- a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemStringParameterType.cs +++ b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemStringParameterType.cs @@ -35,7 +35,7 @@ public static void M1({{typeName}} p1) InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -82,7 +82,7 @@ public void M1(string p1) InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -113,8 +113,8 @@ public static C1 FromObject(object obj) """); } - [TestCase("string", "string", "GetPropertyAsString")] - [TestCase("String", "string", "GetPropertyAsString")] + [TestCase("string", "string", "GetPropertyAsStringNullable")] + [TestCase("String", "string", "GetPropertyAsStringNullable")] [TestCase("char", "char", "GetPropertyAsCharNullable")] [TestCase("Char", "char", "GetPropertyAsCharNullable")] public void CSharpInteropClass_InstanceProperty_WithStringParameterType(string typeName, string interopTypeExpression, string jsObjectMethod) @@ -136,7 +136,7 @@ public class C1 InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -153,7 +153,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P1 = (jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } [JSExport] @@ -183,7 +183,7 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P1 = (jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.{{jsObjectMethod}}("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } } diff --git a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemStringReturnType.cs b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemStringReturnType.cs index 209024be..9580a63e 100644 --- a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemStringReturnType.cs +++ b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemStringReturnType.cs @@ -32,7 +32,7 @@ public static string M1() InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -78,7 +78,7 @@ public string M1() InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable diff --git a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemTaskParameterType.cs b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemTaskParameterType.cs index 40b8c376..75b9ba4c 100644 --- a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemTaskParameterType.cs +++ b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemTaskParameterType.cs @@ -48,7 +48,7 @@ public static void M1(Task<{{typeExpression}}> task) InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -118,11 +118,11 @@ public static void M1(Task task) ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); // Important assertion here, Task required for interop, cannot be simply casted to Task // the return type is void so we cannot await either, hence the TaskCompletionSource-based conversion. - Assert.That(interopClass, Is.EqualTo(""" + AssertEx.EqualOrDiff(interopClass, """ #nullable enable // TypeShim generated TypeScript interop definitions using System; @@ -136,7 +136,7 @@ public partial class C1Interop public static void M1([JSMarshalAs>] Task task) { TaskCompletionSource taskTcs = new(); - task.ContinueWith(t => { + (task).ContinueWith(t => { if (t.IsFaulted) taskTcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) taskTcs.SetCanceled(); else taskTcs.SetResult(MyClassInterop.FromObject(t.Result)); @@ -154,7 +154,7 @@ public static C1 FromObject(object obj) } } -""")); +"""); } [Test] @@ -196,7 +196,7 @@ public static void M1(Task task) ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -212,7 +212,7 @@ public partial class C1Interop public static void M1([JSMarshalAs>] Task task) { TaskCompletionSource taskTcs = new(); - task.ContinueWith(t => { + (task).ContinueWith(t => { if (t.IsFaulted) taskTcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) taskTcs.SetCanceled(); else taskTcs.SetResult(MyClassInterop.FromObject(t.Result)); @@ -259,7 +259,7 @@ public static void M1(Task<{{typeName}}> task) InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); // Note: as there is no known mapping for these types, there is no 'FromObject' mapping, instead just try to cast ("taskTcs.SetResult(({{typeName}})t.Result)") // the user shouldnt be able to do much with the object anyway as its untyped on the JS/TS side. @@ -277,7 +277,7 @@ public partial class C1Interop public static void M1([JSMarshalAs>] Task task) { TaskCompletionSource<{{typeName}}> taskTcs = new(); - task.ContinueWith(t => { + (task).ContinueWith(t => { if (t.IsFaulted) taskTcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) taskTcs.SetCanceled(); else taskTcs.SetResult(({{typeName}})t.Result); @@ -323,7 +323,7 @@ public void M1(Task? p1) InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -376,7 +376,7 @@ public class C1 InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); AssertEx.EqualOrDiff(interopClass, """ #nullable enable @@ -393,7 +393,7 @@ public static object ctor([JSMarshalAs] JSObject jsObject) { return new C1() { - P1 = (jsObject.GetPropertyAsVoidTask("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.GetPropertyAsTaskNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } [JSExport] @@ -423,7 +423,7 @@ public static C1 FromJSObject(JSObject jsObject) { return new C1() { - P1 = (jsObject.GetPropertyAsVoidTask("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject))), + P1 = jsObject.GetPropertyAsTaskNullable("P1") ?? throw new ArgumentException("Non-nullable property 'P1' missing or of invalid type", nameof(jsObject)), }; } } diff --git a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemTaskReturnType.cs b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemTaskReturnType.cs index f78fcec0..eebcaca8 100644 --- a/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemTaskReturnType.cs +++ b/TypeShim.Generator.Tests/CSharp/CSharpInteropClassRendererTests_SystemTaskReturnType.cs @@ -47,7 +47,7 @@ private C1() {} InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -117,7 +117,7 @@ public static Task M1() ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -133,7 +133,7 @@ public partial class C1Interop public static Task M1() { TaskCompletionSource retValTcs = new(); - C1.M1().ContinueWith(t => { + (C1.M1()).ContinueWith(t => { if (t.IsFaulted) retValTcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) retValTcs.SetCanceled(); else retValTcs.SetResult((object)t.Result); @@ -193,7 +193,7 @@ public Task M1() ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -210,7 +210,7 @@ public static Task M1([JSMarshalAs] object instance) { C1 typed_instance = (C1)instance; TaskCompletionSource retValTcs = new(); - typed_instance.M1().ContinueWith(t => { + (typed_instance.M1()).ContinueWith(t => { if (t.IsFaulted) retValTcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) retValTcs.SetCanceled(); else retValTcs.SetResult((object)t.Result); @@ -256,7 +256,7 @@ private C1() {} InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -272,7 +272,7 @@ public partial class C1Interop public static Task M1() { TaskCompletionSource retValTcs = new(); - C1.M1().ContinueWith(t => { + (C1.M1()).ContinueWith(t => { if (t.IsFaulted) retValTcs.SetException(t.Exception.InnerExceptions); else if (t.IsCanceled) retValTcs.SetCanceled(); else retValTcs.SetResult((object)t.Result); @@ -318,7 +318,7 @@ public Task M1() InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable @@ -375,7 +375,7 @@ private C1() {} InteropTypeInfoCache typeCache = new(); ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.CSharp); - string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext).Render(); + string interopClass = new CSharpInteropClassRenderer(classInfo, renderContext, new JSObjectMethodResolver([])).Render(); Assert.That(interopClass, Is.EqualTo(""" #nullable enable diff --git a/TypeShim.Generator.Tests/CSharp/JSObjectExtensionsRendererTests_Properties.cs b/TypeShim.Generator.Tests/CSharp/JSObjectExtensionsRendererTests_Properties.cs new file mode 100644 index 00000000..58d05aae --- /dev/null +++ b/TypeShim.Generator.Tests/CSharp/JSObjectExtensionsRendererTests_Properties.cs @@ -0,0 +1,466 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Newtonsoft.Json.Linq; +using TypeShim.Generator.CSharp; +using TypeShim.Generator.Parsing; +using TypeShim.Shared; + +namespace TypeShim.Generator.Tests.CSharp; + +internal class JSObjectExtensionsRendererTests_Properties +{ + [TestCase("bool", "Boolean", "JSType.Boolean")] + [TestCase("string", "String", "JSType.String")] + [TestCase("char", "Char", "JSType.Number", Ignore = ".NET currently wrongly expects JSType.String for char, which indeed is marshalled as JSType.Number at runtime")] + [TestCase("char", "Char", "JSType.String")] + [TestCase("short", "Int16", "JSType.Number")] + [TestCase("int", "Int32", "JSType.Number")] + [TestCase("long", "Int64", "JSType.Number")] + [TestCase("float", "Single", "JSType.Number")] + [TestCase("double", "Double", "JSType.Number")] + public void JSObjectExtensionsRendererTests_InstanceProperty_WithBooleanType(string csTypeName, string managedSuffix, string jsType) + { + string source = """ + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public {{type}} P1 { get; set; } + } + """.Replace("{{type}}", csTypeName); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses.First(); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + + List types = [classInfo.Properties.First().Type]; + RenderContext extensionsRenderContext = new(classInfo, [classInfo], RenderOptions.CSharp); + new JSObjectExtensionsRenderer(extensionsRenderContext, types).Render(); + + string expected = """ + #nullable enable + // JSImports for the type marshalling process + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + public static partial class JSObjectExtensions + { + public static {{type}}? GetPropertyAs{{managed}}Nullable(this JSObject jsObject, string propertyName) + { + return jsObject.HasProperty(propertyName) ? MarshalAs{{managed}}(jsObject, propertyName) : null; + } + [JSImport("unwrapProperty", "@typeshim")] + [return: JSMarshalAs<{{jstype}}>] + public static partial {{type}} MarshalAs{{managed}}([JSMarshalAs] JSObject obj, [JSMarshalAs] string propertyName); + } + + """ + .Replace("{{type}}", csTypeName) + .Replace("{{managed}}", managedSuffix) + .Replace("{{jstype}}", jsType); + + AssertEx.EqualOrDiff(extensionsRenderContext.ToString(), expected); + } + + [Test] + public void JSObjectExtensionsRendererTests_InstanceProperty_WithUserClassType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class MyClass + { + public void M1() + { + } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public MyClass P1 { get; set; } + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses.First(), typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); + + List types = [classInfo.Properties.First().Type]; + RenderContext extensionsRenderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); + new JSObjectExtensionsRenderer(extensionsRenderContext, types).Render(); + AssertEx.EqualOrDiff(extensionsRenderContext.ToString(), """ + #nullable enable + // JSImports for the type marshalling process + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + public static partial class JSObjectExtensions + { + public static object? GetPropertyAsObjectNullable(this JSObject jsObject, string propertyName) + { + return jsObject.HasProperty(propertyName) ? MarshalAsObject(jsObject, propertyName) : null; + } + [JSImport("unwrapProperty", "@typeshim")] + [return: JSMarshalAs] + public static partial object MarshalAsObject([JSMarshalAs] JSObject obj, [JSMarshalAs] string propertyName); + } + + """); + } + + [Test] + public void JSObjectExtensionsRendererTests_InstanceProperty_WithUserClassArrayType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class MyClass + { + public void M1() + { + } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public MyClass[] P1 { get; set; } + } + """); + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + INamedTypeSymbol classSymbol = exportedClasses.First(); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); + + List types = [classInfo.Properties.First().Type]; + RenderContext extensionsRenderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); + new JSObjectExtensionsRenderer(extensionsRenderContext, types).Render(); + AssertEx.EqualOrDiff(extensionsRenderContext.ToString(), """ + #nullable enable + // JSImports for the type marshalling process + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + public static partial class JSObjectExtensions + { + public static object[]? GetPropertyAsObjectArrayNullable(this JSObject jsObject, string propertyName) + { + return jsObject.HasProperty(propertyName) ? MarshalAsObjectArray(jsObject, propertyName) : null; + } + [JSImport("unwrapProperty", "@typeshim")] + [return: JSMarshalAs>] + public static partial object[] MarshalAsObjectArray([JSMarshalAs] JSObject obj, [JSMarshalAs] string propertyName); + } + + """); + } + + [Test] + public void JSObjectExtensionsRendererTests_InstanceProperty_WithActionType() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Action P1 { get; set; } + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses.First(); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + + List types = [classInfo.Properties.First().Type]; + RenderContext extensionsRenderContext = new(classInfo, [classInfo], RenderOptions.CSharp); + new JSObjectExtensionsRenderer(extensionsRenderContext, types).Render(); + + AssertEx.EqualOrDiff(extensionsRenderContext.ToString(), """ + #nullable enable + // JSImports for the type marshalling process + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + public static partial class JSObjectExtensions + { + public static Action? GetPropertyAsVoidActionNullable(this JSObject jsObject, string propertyName) + { + return jsObject.HasProperty(propertyName) ? MarshalAsVoidAction(jsObject, propertyName) : null; + } + [JSImport("unwrapProperty", "@typeshim")] + [return: JSMarshalAs] + public static partial Action MarshalAsVoidAction([JSMarshalAs] JSObject obj, [JSMarshalAs] string propertyName); + } + + """); + } + + [TestCase("Action", "Int32VoidAction", "JSType.Function")] + [TestCase("Action", "BooleanVoidAction", "JSType.Function")] + [TestCase("Action", "StringVoidAction", "JSType.Function")] + [TestCase("Func", "Int32Function", "JSType.Function")] + [TestCase("Func", "BooleanFunction", "JSType.Function")] + [TestCase("Func", "StringFunction", "JSType.Function")] + [TestCase("Func", "StringStringFunction", "JSType.Function")] + [TestCase("Func", "StringBooleanInt32Function", "JSType.Function")] + public void JSObjectExtensionsRendererTests_InstanceProperty_WithDelegateGenericType(string csTypeName, string managedSuffix, string jsType) + { + string source = """ + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public {{type}} P1 { get; set; } + } + """.Replace("{{type}}", csTypeName); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses.First(); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + + List types = [classInfo.Properties.First().Type]; + RenderContext extensionsRenderContext = new(classInfo, [classInfo], RenderOptions.CSharp); + new JSObjectExtensionsRenderer(extensionsRenderContext, types).Render(); + + string expected = """ + #nullable enable + // JSImports for the type marshalling process + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + public static partial class JSObjectExtensions + { + public static {{type}}? GetPropertyAs{{managed}}Nullable(this JSObject jsObject, string propertyName) + { + return jsObject.HasProperty(propertyName) ? MarshalAs{{managed}}(jsObject, propertyName) : null; + } + [JSImport("unwrapProperty", "@typeshim")] + [return: JSMarshalAs<{{jstype}}>] + public static partial {{type}} MarshalAs{{managed}}([JSMarshalAs] JSObject obj, [JSMarshalAs] string propertyName); + } + + """ + .Replace("{{type}}", csTypeName) + .Replace("{{managed}}", managedSuffix) + .Replace("{{jstype}}", jsType); + + AssertEx.EqualOrDiff(extensionsRenderContext.ToString(), expected); + } + + [TestCase("Func", "Func", "ObjectFunction", "JSType.Function")] + [TestCase("Action", "Action", "ObjectVoidAction", "JSType.Function")] + [TestCase("Func", "Func", "ObjectObjectFunction", "JSType.Function")] + [TestCase("Func", "Func", "ObjectObjectObjectFunction", "JSType.Function")] + [TestCase("Action", "Action", "ObjectObjectVoidAction", "JSType.Function")] + [TestCase("Action", "Action", "ObjectObjectObjectVoidAction", "JSType.Function")] + [TestCase("Func", "Func", "Int32ObjectFunction", "JSType.Function")] + [TestCase("Func", "Func", "StringObjectFunction", "JSType.Function")] + [TestCase("Func", "Func", "BooleanObjectFunction", "JSType.Function")] + [TestCase("Func", "Func", "Int64ObjectFunction", "JSType.Function")] + [TestCase("Func", "Func", "ObjectInt32Function", "JSType.Function")] + [TestCase("Func", "Func", "ObjectStringFunction", "JSType.Function")] + [TestCase("Func", "Func", "ObjectBooleanFunction", "JSType.Function")] + [TestCase("Func", "Func", "ObjectInt64Function", "JSType.Function")] + [TestCase("Action", "Action", "ObjectInt32VoidAction", "JSType.Function")] + [TestCase("Action", "Action", "ObjectStringVoidAction", "JSType.Function")] + [TestCase("Action", "Action", "ObjectBooleanVoidAction", "JSType.Function")] + [TestCase("Action", "Action", "ObjectInt64VoidAction", "JSType.Function")] + public void JSObjectExtensionsRendererTests_InstanceProperty_WithDelegateGenericType_IncludingUserClass( + string exposedTypeName, + string boundaryTypeName, + string managedSuffix, + string jsType) + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class MyClass + { + public void M1() + { + } + } + """); + + string source = """ + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public {{type}} P1 { get; set; } + } + """.Replace("{{type}}", exposedTypeName); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses.First(), typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses.Last(), typeCache).Build(); + + List types = [classInfo.Properties.First().Type]; + RenderContext extensionsRenderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.CSharp); + new JSObjectExtensionsRenderer(extensionsRenderContext, types).Render(); + + string expected = """ + #nullable enable + // JSImports for the type marshalling process + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + public static partial class JSObjectExtensions + { + public static {{type}}? GetPropertyAs{{managed}}Nullable(this JSObject jsObject, string propertyName) + { + return jsObject.HasProperty(propertyName) ? MarshalAs{{managed}}(jsObject, propertyName) : null; + } + [JSImport("unwrapProperty", "@typeshim")] + [return: JSMarshalAs<{{jstype}}>] + public static partial {{type}} MarshalAs{{managed}}([JSMarshalAs] JSObject obj, [JSMarshalAs] string propertyName); + } + + """ + .Replace("{{type}}", boundaryTypeName) + .Replace("{{managed}}", managedSuffix) + .Replace("{{jstype}}", jsType); + + AssertEx.EqualOrDiff(extensionsRenderContext.ToString(), expected); + } + + [TestCase("Func", "Func", "ObjectFunction", "JSType.Function")] + [TestCase("Action", "Action", "ObjectVoidAction", "JSType.Function")] + [TestCase("Func", "Func", "ObjectObjectFunction", "JSType.Function")] + [TestCase("Func", "Func", "ObjectObjectObjectFunction", "JSType.Function")] + [TestCase("Action", "Action", "ObjectObjectVoidAction", "JSType.Function")] + [TestCase("Action", "Action", "ObjectObjectObjectVoidAction", "JSType.Function")] + [TestCase("Func", "Func", "Int32ObjectFunction", "JSType.Function")] + [TestCase("Func", "Func", "StringObjectFunction", "JSType.Function")] + [TestCase("Func", "Func", "BooleanObjectFunction", "JSType.Function")] + [TestCase("Func", "Func", "Int64ObjectFunction", "JSType.Function")] + [TestCase("Func", "Func", "ObjectInt32Function", "JSType.Function")] + [TestCase("Func", "Func", "ObjectStringFunction", "JSType.Function")] + [TestCase("Func", "Func", "ObjectBooleanFunction", "JSType.Function")] + [TestCase("Func", "Func", "ObjectInt64Function", "JSType.Function")] + [TestCase("Action", "Action", "ObjectInt32VoidAction", "JSType.Function")] + [TestCase("Action", "Action", "ObjectStringVoidAction", "JSType.Function")] + [TestCase("Action", "Action", "ObjectBooleanVoidAction", "JSType.Function")] + [TestCase("Action", "Action", "ObjectInt64VoidAction", "JSType.Function")] + public void JSObjectExtensionsRendererTests_InstanceProperty_WithDelegateGenericType_IncludingUnexportedUserClass( + string exposedTypeName, + string boundaryTypeName, + string managedSuffix, + string jsType) + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + //[TSExport] not exported + public class MyClass + { + public void M1() + { + } + } + """); + + string source = """ + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public {{type}} P1 { get; set; } + } + """.Replace("{{type}}", exposedTypeName); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses.First(), typeCache).Build(); + + List types = [classInfo.Properties.First().Type]; + RenderContext extensionsRenderContext = new(classInfo, [classInfo], RenderOptions.CSharp); + new JSObjectExtensionsRenderer(extensionsRenderContext, types).Render(); + + string expected = """ + #nullable enable + // JSImports for the type marshalling process + using System; + using System.Runtime.InteropServices.JavaScript; + using System.Threading.Tasks; + public static partial class JSObjectExtensions + { + public static {{type}}? GetPropertyAs{{managed}}Nullable(this JSObject jsObject, string propertyName) + { + return jsObject.HasProperty(propertyName) ? MarshalAs{{managed}}(jsObject, propertyName) : null; + } + [JSImport("unwrapProperty", "@typeshim")] + [return: JSMarshalAs<{{jstype}}>] + public static partial {{type}} MarshalAs{{managed}}([JSMarshalAs] JSObject obj, [JSMarshalAs] string propertyName); + } + + """ + .Replace("{{type}}", boundaryTypeName) + .Replace("{{managed}}", managedSuffix) + .Replace("{{jstype}}", jsType); + + AssertEx.EqualOrDiff(extensionsRenderContext.ToString(), expected); + } +} diff --git a/TypeShim.Generator.Tests/Parsing/SyntaxTreeParsingTests_Delegates.cs b/TypeShim.Generator.Tests/Parsing/SyntaxTreeParsingTests_Delegates.cs new file mode 100644 index 00000000..e017f34b --- /dev/null +++ b/TypeShim.Generator.Tests/Parsing/SyntaxTreeParsingTests_Delegates.cs @@ -0,0 +1,41 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System; +using System.Collections.Generic; +using System.Text; +using TypeShim.Generator.Parsing; +using TypeShim.Shared; + +namespace TypeShim.Generator.Tests.Parsing; + +internal class SyntaxTreeParsingTests_Delegates +{ + [TestCase("int[]")] + [TestCase("int[]?")] + [TestCase("int?")] + [TestCase("char?")] + public void ClassInfoBuilder_RejectsUnsupportedInnerTypes(string innerType) + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Func<{{innerType}}> func) + { + } + } + """.Replace("{{innerType}}", innerType)); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses[0]; + InteropTypeInfoCache typeCache = new(); + Assert.Throws(() => new ClassInfoBuilder(classSymbol, typeCache).Build()); + } + + +} + diff --git a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassInterfaceRendererTests.cs b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassNamespaceRendererTests.cs similarity index 88% rename from TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassInterfaceRendererTests.cs rename to TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassNamespaceRendererTests.cs index b95fdf16..cb8de188 100644 --- a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassInterfaceRendererTests.cs +++ b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassNamespaceRendererTests.cs @@ -9,7 +9,7 @@ namespace TypeShim.Generator.Tests.TypeScript; -internal class TypeScriptUserClassInterfaceRendererTests +internal class TypeScriptUserClassNamespaceRendererTests { [TestCase("string", "string")] [TestCase("double", "number")] @@ -755,4 +755,104 @@ public class C1 """); } + + [Test] + public void UserClassNamespace_PropertyType_Func_DoesNotRenderInSnapshot() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func P1 { get; set; } + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypeScriptUserClassNamespaceRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export namespace C1 { + export interface Initializer { + P1: (arg0: string) => UserClass; + } + } + + """); + } + + [Test] + public void UserClassNamespace_PropertyType_FuncAndInt_OmitsFuncInSnapshot() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func P1 { get; set; } + public int P2 { get; set; } + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypeScriptUserClassNamespaceRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export namespace C1 { + export interface Initializer { + P1: (arg0: string) => UserClass; + P2: number; + } + export interface Snapshot { + P2: number; + } + export function materialize(proxy: C1): C1.Snapshot { + return { + P2: proxy.P2, + }; + } + } + + """); + } } diff --git a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassInterfaceRendererTests_Constructors.cs b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassNamespaceRendererTests_Constructors.cs similarity index 99% rename from TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassInterfaceRendererTests_Constructors.cs rename to TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassNamespaceRendererTests_Constructors.cs index 5d6c26b3..0bcdc507 100644 --- a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassInterfaceRendererTests_Constructors.cs +++ b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassNamespaceRendererTests_Constructors.cs @@ -9,7 +9,7 @@ namespace TypeShim.Generator.Tests.TypeScript; -internal class TypeScriptUserClassInterfaceRendererTests_Constructors +internal class TypeScriptUserClassNamespaceRendererTests_Constructors { [Test] public void UserClassInterface_PrivateConstructor_InstanceProperty_GeneratesNoInitializer() diff --git a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Char.cs b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Char.cs index 58ccabc0..47adf032 100644 --- a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Char.cs +++ b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Char.cs @@ -43,7 +43,8 @@ export class C1 extends ProxyBase { } public M1(): string { - return String.fromCharCode(TypeShimConfig.exports.N1.C1Interop.M1(this.instance)); + const res = TypeShimConfig.exports.N1.C1Interop.M1(this.instance); + return String.fromCharCode(res); } } @@ -121,7 +122,8 @@ export class C1 extends ProxyBase { } public M1(p1: string): string { - return String.fromCharCode(TypeShimConfig.exports.N1.C1Interop.M1(this.instance, p1.charCodeAt(0))); + const res = TypeShimConfig.exports.N1.C1Interop.M1(this.instance, p1.charCodeAt(0)); + return String.fromCharCode(res); } } @@ -154,20 +156,153 @@ public class C1 new TypescriptUserClassProxyRenderer(renderContext).Render(); AssertEx.EqualOrDiff(renderContext.ToString(), """ -export class C1 extends ProxyBase { - constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); - } + export class C1 extends ProxyBase { + constructor(jsObject: C1.Initializer) { + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: jsObject.P1.charCodeAt(0) })); + } - public get P1(): string { - return String.fromCharCode(TypeShimConfig.exports.N1.C1Interop.get_P1(this.instance)); - } + public get P1(): string { + const res = TypeShimConfig.exports.N1.C1Interop.get_P1(this.instance); + return String.fromCharCode(res); + } - public set P1(value: string) { - TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, value.charCodeAt(0)); - } -} + public set P1(value: string) { + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, value.charCodeAt(0)); + } + } -"""); + """); + } + + [Test] + public void TypeScriptUserClassProxy_InstanceProperty_WithNullableCharType_RendersNumberToStringConversion() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public char? P1 { get; set; } + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses[0]; + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor(jsObject: C1.Initializer) { + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: jsObject.P1 ? jsObject.P1.charCodeAt(0) : null })); + } + + public get P1(): string | null { + const res = TypeShimConfig.exports.N1.C1Interop.get_P1(this.instance); + return res ? String.fromCharCode(res) : null; + } + + public set P1(value: string | null) { + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, value ? value.charCodeAt(0) : null); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_InstanceProperty_WithCharTaskType_RendersNumberToStringConversion() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Task P1 { get; set; } + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses[0]; + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor(jsObject: C1.Initializer) { + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: jsObject.P1.then(e => e.charCodeAt(0)) })); + } + + public get P1(): Promise { + const res = TypeShimConfig.exports.N1.C1Interop.get_P1(this.instance); + return res.then(e => String.fromCharCode(e)); + } + + public set P1(value: Promise) { + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, value.then(e => e.charCodeAt(0))); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_InstanceProperty_WithCharNullableTaskType_RendersNumberToStringConversion() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Task? P1 { get; set; } + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses[0]; + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor(jsObject: C1.Initializer) { + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: jsObject.P1 ? jsObject.P1.then(e => e.charCodeAt(0)) : null })); + } + + public get P1(): Promise | null { + const res = TypeShimConfig.exports.N1.C1Interop.get_P1(this.instance); + return res ? res.then(e => String.fromCharCode(e)) : null; + } + + public set P1(value: Promise | null) { + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, value ? value.then(e => e.charCodeAt(0)) : null); + } + } + + """); } } diff --git a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Delegates.cs b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Delegates.cs new file mode 100644 index 00000000..de8241cf --- /dev/null +++ b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Delegates.cs @@ -0,0 +1,1313 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using NUnit.Framework.Internal; +using TypeShim.Generator.CSharp; +using TypeShim.Generator.Parsing; +using TypeShim.Generator.Typescript; +using TypeShim.Shared; + +namespace TypeShim.Generator.Tests.TypeScript; + +internal class TypeScriptUserClassProxyRendererTests_Delegates +{ + [Test] + public void TypeScriptUserClassProxy_MethodWithAction0ParameterType() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Action callback) => callback(); + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + INamedTypeSymbol classSymbol = exportedClasses[0]; + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(callback: () => void): void { + TypeShimConfig.exports.N1.C1Interop.M1(this.instance, callback); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodWithUserClassParameterType_NonInitializable() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + private UserClass() {} + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Action callback) => callback(new UserClass() { Id = 1 }); + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + INamedTypeSymbol classSymbol = exportedClasses[0]; + INamedTypeSymbol userClassSymbol = exportedClasses[1]; + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(userClassSymbol, typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(callback: (arg0: UserClass) => void): void { + TypeShimConfig.exports.N1.C1Interop.M1(this.instance, (arg0: ManagedObject) => callback(ProxyBase.fromHandle(UserClass, arg0))); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodWithUserClassParameterType_Initializable() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public UserClass() {} + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Action callback) => callback(new UserClass() { Id = 1 }); + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + INamedTypeSymbol classSymbol = exportedClasses[0]; + INamedTypeSymbol userClassSymbol = exportedClasses[1]; + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(userClassSymbol, typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(callback: (arg0: UserClass) => void): void { + TypeShimConfig.exports.N1.C1Interop.M1(this.instance, (arg0: ManagedObject) => callback(ProxyBase.fromHandle(UserClass, arg0))); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodWithUserClassAndPrimitiveParameterTypes() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Action callback) => callback(new UserClass() { Id = 1 }, 2); + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + INamedTypeSymbol classSymbol = exportedClasses[0]; + INamedTypeSymbol userClassSymbol = exportedClasses[1]; + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(userClassSymbol, typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(callback: (arg0: UserClass, arg1: number) => void): void { + TypeShimConfig.exports.N1.C1Interop.M1(this.instance, (arg0: ManagedObject, arg1: number) => callback(ProxyBase.fromHandle(UserClass, arg0), arg1)); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodWithMultipleUserClassAndPrimitiveParameterTypes() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree anotherUserClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class AnotherUserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Action callback) => callback(new UserClass() { Id = 1 }, 2, new AnotherUserClass { Id = 3 }); + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass), CSharpFileInfo.Create(anotherUserClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(3)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + ClassInfo anotherUserClassInfo = new ClassInfoBuilder(exportedClasses[2], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo, anotherUserClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(callback: (arg0: UserClass, arg1: number, arg2: AnotherUserClass) => void): void { + TypeShimConfig.exports.N1.C1Interop.M1(this.instance, (arg0: ManagedObject, arg1: number, arg2: ManagedObject) => callback(ProxyBase.fromHandle(UserClass, arg0), arg1, ProxyBase.fromHandle(AnotherUserClass, arg2))); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodWithFuncUserClassReturnType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func M1() => () => (new UserClass() { Id = 1 }); + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(): () => UserClass { + const res = TypeShimConfig.exports.N1.C1Interop.M1(this.instance); + return () => { const retVal = res(); return ProxyBase.fromHandle(UserClass, retVal) }; + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodWithFuncUserClassParameterAndUserClassReturnType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func M1() => (UserClass src) => (new UserClass() { Id = src.Id }); + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(): (arg0: UserClass | UserClass.Initializer) => UserClass { + const res = TypeShimConfig.exports.N1.C1Interop.M1(this.instance); + return (arg0: UserClass | UserClass.Initializer) => { const retVal = res(arg0 instanceof UserClass ? arg0.instance : arg0); return ProxyBase.fromHandle(UserClass, retVal) }; + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodWithActionUserClassReturnType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Action M1() => (UserClass u) => {}; + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(): (arg0: UserClass | UserClass.Initializer) => void { + const res = TypeShimConfig.exports.N1.C1Interop.M1(this.instance); + return (arg0: UserClass | UserClass.Initializer) => res(arg0 instanceof UserClass ? arg0.instance : arg0); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodWithActionUserClassParameterType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Action action) => action(new UserClass() { Id = 1 }); + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(action: (arg0: UserClass) => void): void { + TypeShimConfig.exports.N1.C1Interop.M1(this.instance, (arg0: ManagedObject) => action(ProxyBase.fromHandle(UserClass, arg0))); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodWithFuncUserClassUserClassParameterType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Func func) {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(func: (arg0: UserClass) => UserClass): void { + TypeShimConfig.exports.N1.C1Interop.M1(this.instance, (arg0: ManagedObject) => { const retVal = func(ProxyBase.fromHandle(UserClass, arg0)); return retVal instanceof UserClass ? retVal.instance : retVal }); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodParameter_FuncUserClassNullableUserClass() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Func func) {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(func: (arg0: UserClass) => UserClass | null): void { + TypeShimConfig.exports.N1.C1Interop.M1(this.instance, (arg0: ManagedObject) => { const retVal = func(ProxyBase.fromHandle(UserClass, arg0)); return retVal ? retVal instanceof UserClass ? retVal.instance : retVal : null }); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodParameterType_NullableFuncUserClassNullableUserClass() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Func? func) {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(func: ((arg0: UserClass) => UserClass | null) | null): void { + TypeShimConfig.exports.N1.C1Interop.M1(this.instance, func ? (arg0: ManagedObject) => { const retVal = func(ProxyBase.fromHandle(UserClass, arg0)); return retVal ? retVal instanceof UserClass ? retVal.instance : retVal : null } : null); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodReturnType_NullableFuncUserClassNullableUserClass() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func? M1() {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(): ((arg0: UserClass | UserClass.Initializer) => UserClass | null) | null { + const res = TypeShimConfig.exports.N1.C1Interop.M1(this.instance); + return res ? (arg0: UserClass | UserClass.Initializer) => { const retVal = res(arg0 instanceof UserClass ? arg0.instance : arg0); return retVal ? ProxyBase.fromHandle(UserClass, retVal) : null } : null; + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodReturnType_NullableFuncUserClassUserClass() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func? M1() {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(): ((arg0: UserClass | UserClass.Initializer) => UserClass) | null { + const res = TypeShimConfig.exports.N1.C1Interop.M1(this.instance); + return res ? (arg0: UserClass | UserClass.Initializer) => { const retVal = res(arg0 instanceof UserClass ? arg0.instance : arg0); return ProxyBase.fromHandle(UserClass, retVal) } : null; + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodWithPrimitiveDelegateReturnType() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func M1() => () => 1; + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(): () => number { + return TypeShimConfig.exports.N1.C1Interop.M1(this.instance); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodReturnType_DelegateCharReturn_RendersConversion() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func M1() {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(): () => string { + const res = TypeShimConfig.exports.N1.C1Interop.M1(this.instance); + return () => String.fromCharCode(res()); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodReturnType_DelegateCharParameter_RendersConversion() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Action M1() {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(): (arg0: string) => void { + const res = TypeShimConfig.exports.N1.C1Interop.M1(this.instance); + return (arg0: string) => res(arg0.charCodeAt(0)); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodParameterType_DelegateCharReturn_RendersConversion() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Func func) {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(func: () => string): void { + TypeShimConfig.exports.N1.C1Interop.M1(this.instance, () => func().charCodeAt(0)); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodParameterType_DelegateCharParameter_RendersConversion() + { + // TODO: Add tests for delegate+char in initializer + // TODO: Add tests for delegate+char+proxy in initializer + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Action func) {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(func: (arg0: string) => void): void { + TypeShimConfig.exports.N1.C1Interop.M1(this.instance, (arg0: number) => func(String.fromCharCode(arg0))); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodParameterType_DelegateCharAndUserClassParameter_RendersBothConversions() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Action func) {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(func: (arg0: string, arg1: UserClass) => void): void { + TypeShimConfig.exports.N1.C1Interop.M1(this.instance, (arg0: number, arg1: ManagedObject) => func(String.fromCharCode(arg0), ProxyBase.fromHandle(UserClass, arg1))); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodReturnType_DelegateCharAndUserClassParameter_RendersBothConversions() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Action M1() {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(): (arg0: string, arg1: UserClass | UserClass.Initializer) => void { + const res = TypeShimConfig.exports.N1.C1Interop.M1(this.instance); + return (arg0: string, arg1: UserClass | UserClass.Initializer) => res(arg0.charCodeAt(0), arg1 instanceof UserClass ? arg1.instance : arg1); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_PropertyType_ActionCharAndUserClassParameter_RendersBothConversions() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Action P1 { get; set; } + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor(jsObject: C1.Initializer) { + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: (arg0: number, arg1: ManagedObject) => jsObject.P1(String.fromCharCode(arg0), ProxyBase.fromHandle(UserClass, arg1)) })); + } + + public get P1(): (arg0: string, arg1: UserClass | UserClass.Initializer) => void { + const res = TypeShimConfig.exports.N1.C1Interop.get_P1(this.instance); + return (arg0: string, arg1: UserClass | UserClass.Initializer) => res(arg0.charCodeAt(0), arg1 instanceof UserClass ? arg1.instance : arg1); + } + + public set P1(value: (arg0: string, arg1: UserClass) => void) { + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, (arg0: number, arg1: ManagedObject) => value(String.fromCharCode(arg0), ProxyBase.fromHandle(UserClass, arg1))); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_PropertyType_FuncCharAndUserClassParameter_RendersBothConversions() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func P1 { get; set; } + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor(jsObject: C1.Initializer) { + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: (arg0: number) => { const retVal = jsObject.P1(String.fromCharCode(arg0)); return retVal instanceof UserClass ? retVal.instance : retVal } })); + } + + public get P1(): (arg0: string) => UserClass { + const res = TypeShimConfig.exports.N1.C1Interop.get_P1(this.instance); + return (arg0: string) => { const retVal = res(arg0.charCodeAt(0)); return ProxyBase.fromHandle(UserClass, retVal) }; + } + + public set P1(value: (arg0: string) => UserClass) { + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, (arg0: number) => { const retVal = value(String.fromCharCode(arg0)); return retVal instanceof UserClass ? retVal.instance : retVal }); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_PropertyType_DelegateCharParameter_RendersConversions() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Action P1 { get; set; } + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor(jsObject: C1.Initializer) { + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: (arg0: number) => jsObject.P1(String.fromCharCode(arg0)) })); + } + + public get P1(): (arg0: string) => void { + const res = TypeShimConfig.exports.N1.C1Interop.get_P1(this.instance); + return (arg0: string) => res(arg0.charCodeAt(0)); + } + + public set P1(value: (arg0: string) => void) { + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, (arg0: number) => value(String.fromCharCode(arg0))); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_PropertyType_DelegateUserClassParameter_RendersConversions() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Action P1 { get; set; } + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(exportedClasses[1], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor(jsObject: C1.Initializer) { + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: (arg0: ManagedObject) => jsObject.P1(ProxyBase.fromHandle(UserClass, arg0)) })); + } + + public get P1(): (arg0: UserClass | UserClass.Initializer) => void { + const res = TypeShimConfig.exports.N1.C1Interop.get_P1(this.instance); + return (arg0: UserClass | UserClass.Initializer) => res(arg0 instanceof UserClass ? arg0.instance : arg0); + } + + public set P1(value: (arg0: UserClass) => void) { + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, (arg0: ManagedObject) => value(ProxyBase.fromHandle(UserClass, arg0))); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodWithPrimitiveDelegatePropertyAndReturnType() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func M1() => (bool b) => b ? 1 : 0; + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(): (arg0: boolean) => number { + return TypeShimConfig.exports.N1.C1Interop.M1(this.instance); + } + } + + """); + } + + [Test] + public void TypeScriptUserClassProxy_MethodWithPrimitiveParameterAndPrimitiveDelegateParameterAndReturnType() + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func M1(string s) => (bool b) => b ? 1 : 0; + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(1)); + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(exportedClasses[0], typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ + export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public M1(s: string): (arg0: boolean) => number { + return TypeShimConfig.exports.N1.C1Interop.M1(this.instance, s); + } + } + + """); + } + + +} diff --git a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Methods.cs b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Methods.cs index 747527ac..5d37fefd 100644 --- a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Methods.cs +++ b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Methods.cs @@ -243,8 +243,60 @@ export class C1 extends ProxyBase { } public DoStuff(u: UserClass | UserClass.Initializer): void { - const uInstance = u instanceof UserClass ? u.instance : u; - TypeShimConfig.exports.N1.C1Interop.DoStuff(this.instance, uInstance); + TypeShimConfig.exports.N1.C1Interop.DoStuff(this.instance, u instanceof UserClass ? u.instance : u); + } +} + +"""); + } + + [Test] + public void TypeScriptUserClassProxy_InstanceMethod_WithNonInitializableUserClassParameterType_ExtractsInstancePropertyInline() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + private UserClass() {} // Non-initializable + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void DoStuff(UserClass u) {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + INamedTypeSymbol classSymbol = exportedClasses[0]; + INamedTypeSymbol userClassSymbol = exportedClasses[1]; + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(userClassSymbol, typeCache).Build(); + + RenderContext renderContext = new(classInfo, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptUserClassProxyRenderer(renderContext).Render(); + + AssertEx.EqualOrDiff(renderContext.ToString(), """ +export class C1 extends ProxyBase { + constructor() { + super(TypeShimConfig.exports.N1.C1Interop.ctor()); + } + + public DoStuff(u: UserClass): void { + TypeShimConfig.exports.N1.C1Interop.DoStuff(this.instance, u.instance); } } @@ -296,9 +348,7 @@ export class C1 extends ProxyBase { } public DoStuff(u: UserClass | UserClass.Initializer, v: UserClass | UserClass.Initializer): void { - const uInstance = u instanceof UserClass ? u.instance : u; - const vInstance = v instanceof UserClass ? v.instance : v; - TypeShimConfig.exports.N1.C1Interop.DoStuff(this.instance, uInstance, vInstance); + TypeShimConfig.exports.N1.C1Interop.DoStuff(this.instance, u instanceof UserClass ? u.instance : u, v instanceof UserClass ? v.instance : v); } } @@ -350,8 +400,7 @@ export class C1 extends ProxyBase { } public DoStuff(u: UserClass | UserClass.Initializer | null): void { - const uInstance = u ? u instanceof UserClass ? u.instance : u : null; - TypeShimConfig.exports.N1.C1Interop.DoStuff(this.instance, uInstance); + TypeShimConfig.exports.N1.C1Interop.DoStuff(this.instance, u ? u instanceof UserClass ? u.instance : u : null); } } @@ -403,8 +452,7 @@ export class C1 extends ProxyBase { } public DoStuff(u: Array): void { - const uInstance = u.map(e => e instanceof UserClass ? e.instance : e); - TypeShimConfig.exports.N1.C1Interop.DoStuff(this.instance, uInstance); + TypeShimConfig.exports.N1.C1Interop.DoStuff(this.instance, u.map(e => e instanceof UserClass ? e.instance : e)); } } @@ -456,8 +504,7 @@ export class C1 extends ProxyBase { } public DoStuff(u: Promise): void { - const uInstance = u.then(e => e instanceof UserClass ? e.instance : e); - TypeShimConfig.exports.N1.C1Interop.DoStuff(this.instance, uInstance); + TypeShimConfig.exports.N1.C1Interop.DoStuff(this.instance, u.then(e => e instanceof UserClass ? e.instance : e)); } } diff --git a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_ParameterizedConstructors.cs b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_ParameterizedConstructors.cs index 98466523..785262d5 100644 --- a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_ParameterizedConstructors.cs +++ b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_ParameterizedConstructors.cs @@ -131,8 +131,7 @@ public class C1(UserClass p1) AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(p1: UserClass | UserClass.Initializer) { - const p1Instance = p1 instanceof UserClass ? p1.instance : p1; - super(TypeShimConfig.exports.N1.C1Interop.ctor(p1Instance)); + super(TypeShimConfig.exports.N1.C1Interop.ctor(p1 instanceof UserClass ? p1.instance : p1)); } } @@ -180,8 +179,7 @@ public class C1(UserClass? p1) AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(p1: UserClass | UserClass.Initializer | null) { - const p1Instance = p1 ? p1 instanceof UserClass ? p1.instance : p1 : null; - super(TypeShimConfig.exports.N1.C1Interop.ctor(p1Instance)); + super(TypeShimConfig.exports.N1.C1Interop.ctor(p1 ? p1 instanceof UserClass ? p1.instance : p1 : null)); } } @@ -229,8 +227,7 @@ public class C1(UserClass[] p1) AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(p1: Array) { - const p1Instance = p1.map(e => e instanceof UserClass ? e.instance : e); - super(TypeShimConfig.exports.N1.C1Interop.ctor(p1Instance)); + super(TypeShimConfig.exports.N1.C1Interop.ctor(p1.map(e => e instanceof UserClass ? e.instance : e))); } } @@ -278,8 +275,7 @@ public class C1(UserClass?[] p1) AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(p1: Array) { - const p1Instance = p1.map(e => e ? e instanceof UserClass ? e.instance : e : null); - super(TypeShimConfig.exports.N1.C1Interop.ctor(p1Instance)); + super(TypeShimConfig.exports.N1.C1Interop.ctor(p1.map(e => e ? e instanceof UserClass ? e.instance : e : null))); } } @@ -327,8 +323,7 @@ public class C1(UserClass[]? p1) AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(p1: Array | null) { - const p1Instance = p1 ? p1.map(e => e instanceof UserClass ? e.instance : e) : null; - super(TypeShimConfig.exports.N1.C1Interop.ctor(p1Instance)); + super(TypeShimConfig.exports.N1.C1Interop.ctor(p1 ? p1.map(e => e instanceof UserClass ? e.instance : e) : null)); } } @@ -376,8 +371,7 @@ public class C1(UserClass?[]? p1) AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(p1: Array | null) { - const p1Instance = p1 ? p1.map(e => e ? e instanceof UserClass ? e.instance : e : null) : null; - super(TypeShimConfig.exports.N1.C1Interop.ctor(p1Instance)); + super(TypeShimConfig.exports.N1.C1Interop.ctor(p1 ? p1.map(e => e ? e instanceof UserClass ? e.instance : e : null) : null)); } } @@ -425,8 +419,7 @@ public class C1(Task p1) AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(p1: Promise) { - const p1Instance = p1.then(e => e instanceof UserClass ? e.instance : e); - super(TypeShimConfig.exports.N1.C1Interop.ctor(p1Instance)); + super(TypeShimConfig.exports.N1.C1Interop.ctor(p1.then(e => e instanceof UserClass ? e.instance : e))); } } @@ -474,8 +467,7 @@ public class C1(Task p1) AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(p1: Promise) { - const p1Instance = p1.then(e => e ? e instanceof UserClass ? e.instance : e : null); - super(TypeShimConfig.exports.N1.C1Interop.ctor(p1Instance)); + super(TypeShimConfig.exports.N1.C1Interop.ctor(p1.then(e => e ? e instanceof UserClass ? e.instance : e : null))); } } @@ -523,8 +515,7 @@ public class C1(Task? p1) AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(p1: Promise | null) { - const p1Instance = p1 ? p1.then(e => e instanceof UserClass ? e.instance : e) : null; - super(TypeShimConfig.exports.N1.C1Interop.ctor(p1Instance)); + super(TypeShimConfig.exports.N1.C1Interop.ctor(p1 ? p1.then(e => e instanceof UserClass ? e.instance : e) : null)); } } @@ -572,8 +563,7 @@ public class C1(Task? p1) AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(p1: Promise | null) { - const p1Instance = p1 ? p1.then(e => e ? e instanceof UserClass ? e.instance : e : null) : null; - super(TypeShimConfig.exports.N1.C1Interop.ctor(p1Instance)); + super(TypeShimConfig.exports.N1.C1Interop.ctor(p1 ? p1.then(e => e ? e instanceof UserClass ? e.instance : e : null) : null)); } } @@ -609,7 +599,7 @@ public class C1 AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject })); } public get P1(): ManagedObject { @@ -652,7 +642,7 @@ public class C1(int i) AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(i: number, jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(i, jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor(i, { ...jsObject })); } public get P1(): ManagedObject { diff --git a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_ParameterlessConstructors.cs b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_ParameterlessConstructors.cs index ca734a6f..be1b27eb 100644 --- a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_ParameterlessConstructors.cs +++ b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_ParameterlessConstructors.cs @@ -39,7 +39,7 @@ public class C1() AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject })); } public get P1(): {{typeScriptType}} { diff --git a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Properties.cs b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Properties.cs index b523b9fe..95f8e880 100644 --- a/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Properties.cs +++ b/TypeShim.Generator.Tests/TypeScript/TypeScriptUserClassProxyRendererTests_Properties.cs @@ -41,7 +41,7 @@ public class C1 AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject })); } public get P1(): {{typeScriptType}} { @@ -86,7 +86,7 @@ public class C1 AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject })); } public get P1(): {{typeScriptType}} { @@ -127,7 +127,7 @@ public class C1 AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject })); } public static get P1(): {{typeScriptType}} { @@ -245,7 +245,7 @@ public class C1 AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: jsObject.P1 instanceof UserClass ? jsObject.P1.instance : jsObject.P1 })); } public get P1(): UserClass { @@ -254,8 +254,7 @@ public get P1(): UserClass { } public set P1(value: UserClass | UserClass.Initializer) { - const valueInstance = value instanceof UserClass ? value.instance : value; - TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, valueInstance); + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, value instanceof UserClass ? value.instance : value); } } @@ -310,8 +309,7 @@ public static get P1(): UserClass { } public static set P1(value: UserClass | UserClass.Initializer) { - const valueInstance = value instanceof UserClass ? value.instance : value; - TypeShimConfig.exports.N1.C1Interop.set_P1(valueInstance); + TypeShimConfig.exports.N1.C1Interop.set_P1(value instanceof UserClass ? value.instance : value); } } @@ -359,7 +357,7 @@ public class C1 AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: jsObject.P1 ? jsObject.P1 instanceof UserClass ? jsObject.P1.instance : jsObject.P1 : null })); } public get P1(): UserClass | null { @@ -368,8 +366,7 @@ public get P1(): UserClass | null { } public set P1(value: UserClass | UserClass.Initializer | null) { - const valueInstance = value ? value instanceof UserClass ? value.instance : value : null; - TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, valueInstance); + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, value ? value instanceof UserClass ? value.instance : value : null); } } @@ -417,7 +414,7 @@ public class C1 AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: jsObject.P1.then(e => e instanceof UserClass ? e.instance : e) })); } public get P1(): Promise { @@ -426,8 +423,7 @@ public get P1(): Promise { } public set P1(value: Promise) { - const valueInstance = value.then(e => e instanceof UserClass ? e.instance : e); - TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, valueInstance); + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, value.then(e => e instanceof UserClass ? e.instance : e)); } } @@ -475,7 +471,7 @@ public class C1 AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject })); } public get P1(): Promise { @@ -531,7 +527,7 @@ public class C1 AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: jsObject.P1.map(e => e instanceof UserClass ? e.instance : e) })); } public get P1(): Array { @@ -540,8 +536,7 @@ public get P1(): Array { } public set P1(value: Array) { - const valueInstance = value.map(e => e instanceof UserClass ? e.instance : e); - TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, valueInstance); + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, value.map(e => e instanceof UserClass ? e.instance : e)); } } @@ -589,7 +584,7 @@ public class C1 AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: jsObject.P1.map(e => e ? e instanceof UserClass ? e.instance : e : null) })); } public get P1(): Array { @@ -598,8 +593,7 @@ public get P1(): Array { } public set P1(value: Array) { - const valueInstance = value.map(e => e ? e instanceof UserClass ? e.instance : e : null); - TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, valueInstance); + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, value.map(e => e ? e instanceof UserClass ? e.instance : e : null)); } } @@ -647,7 +641,7 @@ public class C1 AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: jsObject.P1 ? jsObject.P1.map(e => e instanceof UserClass ? e.instance : e) : null })); } public get P1(): Array | null { @@ -656,8 +650,7 @@ public get P1(): Array | null { } public set P1(value: Array | null) { - const valueInstance = value ? value.map(e => e instanceof UserClass ? e.instance : e) : null; - TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, valueInstance); + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, value ? value.map(e => e instanceof UserClass ? e.instance : e) : null); } } @@ -705,7 +698,7 @@ public class C1 AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: jsObject.P1 ? jsObject.P1.map(e => e ? e instanceof UserClass ? e.instance : e : null) : null })); } public get P1(): Array | null { @@ -714,8 +707,7 @@ public get P1(): Array | null { } public set P1(value: Array | null) { - const valueInstance = value ? value.map(e => e ? e instanceof UserClass ? e.instance : e : null) : null; - TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, valueInstance); + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, value ? value.map(e => e ? e instanceof UserClass ? e.instance : e : null) : null); } } @@ -763,7 +755,7 @@ public class C1 AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: jsObject.P1.then(e => e ? e instanceof UserClass ? e.instance : e : null) })); } public get P1(): Promise { @@ -772,8 +764,7 @@ public get P1(): Promise { } public set P1(value: Promise) { - const valueInstance = value.then(e => e ? e instanceof UserClass ? e.instance : e : null); - TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, valueInstance); + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, value.then(e => e ? e instanceof UserClass ? e.instance : e : null)); } } @@ -821,7 +812,7 @@ public class C1 AssertEx.EqualOrDiff(renderContext.ToString(), """ export class C1 extends ProxyBase { constructor(jsObject: C1.Initializer) { - super(TypeShimConfig.exports.N1.C1Interop.ctor(jsObject)); + super(TypeShimConfig.exports.N1.C1Interop.ctor({ ...jsObject, P1: jsObject.P1 ? jsObject.P1.then(e => e ? e instanceof UserClass ? e.instance : e : null) : null })); } public get P1(): Promise | null { @@ -830,8 +821,7 @@ public get P1(): Promise | null { } public set P1(value: Promise | null) { - const valueInstance = value ? value.then(e => e ? e instanceof UserClass ? e.instance : e : null) : null; - TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, valueInstance); + TypeShimConfig.exports.N1.C1Interop.set_P1(this.instance, value ? value.then(e => e ? e instanceof UserClass ? e.instance : e : null) : null); } } diff --git a/TypeShim.Generator.Tests/TypeScript/TypescriptAssemblyExportsRendererTests.cs b/TypeShim.Generator.Tests/TypeScript/TypescriptAssemblyExportsRendererTests.cs index 2338b567..9652f417 100644 --- a/TypeShim.Generator.Tests/TypeScript/TypescriptAssemblyExportsRendererTests.cs +++ b/TypeShim.Generator.Tests/TypeScript/TypescriptAssemblyExportsRendererTests.cs @@ -621,6 +621,238 @@ export interface AssemblyExports{ }; } +"""); + } + + [Test] + public void TypeScriptInteropInterfaceRenderer_InstanceMethod_WithFuncUserClassParameterType_HasFunctionType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Func u) {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + INamedTypeSymbol classSymbol = exportedClasses[0]; + INamedTypeSymbol userClassSymbol = exportedClasses[1]; + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(userClassSymbol, typeCache).Build(); + + ModuleHierarchyInfo hierarchyInfo = ModuleHierarchyInfo.FromClasses([classInfo, userClassInfo]); + RenderContext renderCtx = new(null, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptAssemblyExportsRenderer(hierarchyInfo, renderCtx).Render(); + + AssertEx.EqualOrDiff(renderCtx.ToString(), """ +// TypeShim generated TypeScript module exports interface +export interface AssemblyExports{ + N1: { + C1Interop: { + ctor(): ManagedObject; + M1(instance: ManagedObject, u: () => ManagedObject): void; + }; + UserClassInterop: { + ctor(jsObject: object): ManagedObject; + get_Id(instance: ManagedObject): number; + set_Id(instance: ManagedObject, value: number): void; + }; + }; +} + +"""); + } + + [Test] + public void TypeScriptInteropInterfaceRenderer_InstanceMethod_WithFuncUserClassUserClassParameterType_HasFunctionType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public void M1(Func u) {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + INamedTypeSymbol classSymbol = exportedClasses[0]; + INamedTypeSymbol userClassSymbol = exportedClasses[1]; + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(userClassSymbol, typeCache).Build(); + + ModuleHierarchyInfo hierarchyInfo = ModuleHierarchyInfo.FromClasses([classInfo, userClassInfo]); + RenderContext renderCtx = new(null, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptAssemblyExportsRenderer(hierarchyInfo, renderCtx).Render(); + + AssertEx.EqualOrDiff(renderCtx.ToString(), """ +// TypeShim generated TypeScript module exports interface +export interface AssemblyExports{ + N1: { + C1Interop: { + ctor(): ManagedObject; + M1(instance: ManagedObject, u: (arg0: ManagedObject) => ManagedObject): void; + }; + UserClassInterop: { + ctor(jsObject: object): ManagedObject; + get_Id(instance: ManagedObject): number; + set_Id(instance: ManagedObject, value: number): void; + }; + }; +} + +"""); + } + + [Test] + public void TypeScriptInteropInterfaceRenderer_InstanceMethod_WithFuncUserClassReturnType_HasFunctionType() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func M1() {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + INamedTypeSymbol classSymbol = exportedClasses[0]; + INamedTypeSymbol userClassSymbol = exportedClasses[1]; + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(userClassSymbol, typeCache).Build(); + + ModuleHierarchyInfo hierarchyInfo = ModuleHierarchyInfo.FromClasses([classInfo, userClassInfo]); + RenderContext renderCtx = new(null, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptAssemblyExportsRenderer(hierarchyInfo, renderCtx).Render(); + + AssertEx.EqualOrDiff(renderCtx.ToString(), """ +// TypeShim generated TypeScript module exports interface +export interface AssemblyExports{ + N1: { + C1Interop: { + ctor(): ManagedObject; + M1(instance: ManagedObject): () => ManagedObject; + }; + UserClassInterop: { + ctor(jsObject: object): ManagedObject; + get_Id(instance: ManagedObject): number; + set_Id(instance: ManagedObject, value: number): void; + }; + }; +} + +"""); + } + + [Test] + public void TypeScriptInteropInterfaceRenderer_InstanceMethod_WithFuncUserClassUserClassReturnType__WithFuncUserClassUserClassParameterType_HasFunctionTypeWithInitializerUnionInParameter() + { + SyntaxTree userClass = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class UserClass + { + public int Id { get; set; } + } + """); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(""" + using System; + using System.Threading.Tasks; + namespace N1; + [TSExport] + public class C1 + { + public Func M1() {} + } + """); + + SymbolExtractor symbolExtractor = new([CSharpFileInfo.Create(syntaxTree), CSharpFileInfo.Create(userClass)]); + List exportedClasses = [.. symbolExtractor.ExtractAllExportedSymbols()]; + Assert.That(exportedClasses, Has.Count.EqualTo(2)); + INamedTypeSymbol classSymbol = exportedClasses[0]; + INamedTypeSymbol userClassSymbol = exportedClasses[1]; + + InteropTypeInfoCache typeCache = new(); + ClassInfo classInfo = new ClassInfoBuilder(classSymbol, typeCache).Build(); + ClassInfo userClassInfo = new ClassInfoBuilder(userClassSymbol, typeCache).Build(); + + ModuleHierarchyInfo hierarchyInfo = ModuleHierarchyInfo.FromClasses([classInfo, userClassInfo]); + RenderContext renderCtx = new(null, [classInfo, userClassInfo], RenderOptions.TypeScript); + new TypescriptAssemblyExportsRenderer(hierarchyInfo, renderCtx).Render(); + + AssertEx.EqualOrDiff(renderCtx.ToString(), """ +// TypeShim generated TypeScript module exports interface +export interface AssemblyExports{ + N1: { + C1Interop: { + ctor(): ManagedObject; + M1(instance: ManagedObject): (arg0: ManagedObject | object) => ManagedObject; + }; + UserClassInterop: { + ctor(jsObject: object): ManagedObject; + get_Id(instance: ManagedObject): number; + set_Id(instance: ManagedObject, value: number): void; + }; + }; +} + """); } } diff --git a/TypeShim.Generator/CSharp/CSharpInteropClassRenderer.cs b/TypeShim.Generator/CSharp/CSharpInteropClassRenderer.cs index 136621f7..c17b8a22 100644 --- a/TypeShim.Generator/CSharp/CSharpInteropClassRenderer.cs +++ b/TypeShim.Generator/CSharp/CSharpInteropClassRenderer.cs @@ -12,7 +12,7 @@ internal sealed class CSharpInteropClassRenderer private readonly CSharpTypeConversionRenderer _conversionRenderer; private readonly CSharpMethodRenderer _methodRenderer; - public CSharpInteropClassRenderer(ClassInfo classInfo, RenderContext context) + public CSharpInteropClassRenderer(ClassInfo classInfo, RenderContext context, JSObjectMethodResolver methodResolver) { ArgumentNullException.ThrowIfNull(classInfo); ArgumentNullException.ThrowIfNull(context); @@ -23,7 +23,7 @@ public CSharpInteropClassRenderer(ClassInfo classInfo, RenderContext context) _classInfo = classInfo; _ctx = context; _conversionRenderer = new CSharpTypeConversionRenderer(context); - _methodRenderer = new CSharpMethodRenderer(context, _conversionRenderer); + _methodRenderer = new CSharpMethodRenderer(context, _conversionRenderer, methodResolver); } internal string Render() diff --git a/TypeShim.Generator/CSharp/CSharpMethodRenderer.cs b/TypeShim.Generator/CSharp/CSharpMethodRenderer.cs index 893ea812..69953ab0 100644 --- a/TypeShim.Generator/CSharp/CSharpMethodRenderer.cs +++ b/TypeShim.Generator/CSharp/CSharpMethodRenderer.cs @@ -1,11 +1,12 @@ using Microsoft.CodeAnalysis; using System.Data; +using System.Diagnostics; using System.Reflection; using TypeShim.Shared; namespace TypeShim.Generator.CSharp; -internal sealed class CSharpMethodRenderer(RenderContext _ctx, CSharpTypeConversionRenderer _conversionRenderer) +internal sealed class CSharpMethodRenderer(RenderContext _ctx, CSharpTypeConversionRenderer _conversionRenderer, JSObjectMethodResolver _methodResolver) { internal void RenderConstructorMethod(ConstructorInfo constructorInfo) { @@ -48,9 +49,9 @@ internal void RenderMethod(MethodInfo methodInfo) private void RenderConstructorMethodCore(ConstructorInfo constructorInfo) { - JSMarshalAsAttributeRenderer marshalAsAttributeRenderer = new(constructorInfo.Type); + JSMarshalAsAttributeRenderer marshalAsAttributeRenderer = new(); _ctx.AppendLine(marshalAsAttributeRenderer.RenderJSExportAttribute().NormalizeWhitespace().ToFullString()) - .AppendLine(marshalAsAttributeRenderer.RenderReturnAttribute().NormalizeWhitespace().ToFullString()); + .AppendLine(marshalAsAttributeRenderer.RenderReturnAttribute(constructorInfo.Type.JSTypeSyntax).NormalizeWhitespace().ToFullString()); MethodParameterInfo[] allParameters = constructorInfo.GetParametersIncludingInitializerObject(); RenderMethodSignature(constructorInfo.Name, constructorInfo.Type, allParameters); @@ -69,9 +70,9 @@ private void RenderConstructorMethodCore(ConstructorInfo constructorInfo) private void RenderMethodCore(MethodInfo methodInfo) { - JSMarshalAsAttributeRenderer marshalAsAttributeRenderer = new(methodInfo.ReturnType); + JSMarshalAsAttributeRenderer marshalAsAttributeRenderer = new(); _ctx.AppendLine(marshalAsAttributeRenderer.RenderJSExportAttribute().NormalizeWhitespace().ToFullString()) - .AppendLine(marshalAsAttributeRenderer.RenderReturnAttribute().NormalizeWhitespace().ToFullString()); + .AppendLine(marshalAsAttributeRenderer.RenderReturnAttribute(methodInfo.ReturnType.JSTypeSyntax).NormalizeWhitespace().ToFullString()); RenderMethodSignature(methodInfo.Name, methodInfo.ReturnType, methodInfo.Parameters); _ctx.AppendLine("{"); @@ -81,20 +82,46 @@ private void RenderMethodCore(MethodInfo methodInfo) { _conversionRenderer.RenderParameterTypeConversion(originalParamInfo); } - RenderUserMethodInvocation(methodInfo); - } + DeferredExpressionRenderer returnValueExpression = _conversionRenderer.RenderReturnTypeConversion(methodInfo.ReturnType, DeferredExpressionRenderer.From(RenderInvocationExpression)); + if (methodInfo.ReturnType.ManagedType != KnownManagedType.Void) _ctx.Append("return "); + returnValueExpression.Render(); + _ctx.AppendLine(";"); + } _ctx.AppendLine("}"); + + void RenderInvocationExpression() + { + IReadOnlyCollection parameters = methodInfo.Parameters; + if (methodInfo.IsStatic) + { + _ctx.Append(_ctx.Class.Name); + } + else + { + _ctx.Append(_ctx.LocalScope.GetAccessorExpression(methodInfo.Parameters.First(p => p.IsInjectedInstanceParameter))); + parameters = [.. methodInfo.Parameters.Skip(1)]; + } + + _ctx.Append('.').Append(methodInfo.Name).Append('('); + bool isFirst = true; + foreach (MethodParameterInfo param in parameters) + { + if (!isFirst) _ctx.Append(", "); + _ctx.Append(_ctx.LocalScope.GetAccessorExpression(param)); + isFirst = false; + } + _ctx.Append(")"); + } } private void RenderPropertyMethodCore(PropertyInfo propertyInfo, MethodInfo methodInfo) { - JSMarshalAsAttributeRenderer marshalAsAttributeRenderer = new(methodInfo.ReturnType); + JSMarshalAsAttributeRenderer marshalAsAttributeRenderer = new(); _ctx.AppendLine(marshalAsAttributeRenderer.RenderJSExportAttribute().NormalizeWhitespace().ToFullString()); - _ctx.AppendLine(marshalAsAttributeRenderer.RenderReturnAttribute().NormalizeWhitespace().ToFullString()); + _ctx.AppendLine(marshalAsAttributeRenderer.RenderReturnAttribute(methodInfo.ReturnType.JSTypeSyntax).NormalizeWhitespace().ToFullString()); RenderMethodSignature(methodInfo.Name, methodInfo.ReturnType, methodInfo.Parameters); - _ctx.AppendLine("{"); using (_ctx.Indent()) @@ -105,29 +132,19 @@ private void RenderPropertyMethodCore(PropertyInfo propertyInfo, MethodInfo meth } string accessedObject = methodInfo.IsStatic ? _ctx.Class.Name : _ctx.LocalScope.GetAccessorExpression(methodInfo.Parameters.ElementAt(0)); - string accessorExpression = $"{accessedObject}.{propertyInfo.Name}"; - - if (methodInfo.ReturnType is { IsNullableType: true, TypeArgument.IsTaskType: true }) - { - // Handle Task? property conversion to interop type Task? - string convertedTaskExpression = _conversionRenderer.RenderNullableTaskTypeConversion(methodInfo.ReturnType.AsInteropTypeInfo(), "retVal", accessorExpression); - accessorExpression = convertedTaskExpression; // continue with the converted expression - } - else if (methodInfo.ReturnType is { IsTaskType: true, TypeArgument.RequiresTypeConversion: true }) - { - // Handle Task property conversion to interop type Task - string convertedTaskExpression = _conversionRenderer.RenderTaskTypeConversion(methodInfo.ReturnType.AsInteropTypeInfo(), "retVal", accessorExpression); - accessorExpression = convertedTaskExpression; // continue with the converted expression - } - + DeferredExpressionRenderer untypedValueExpressionRenderer = DeferredExpressionRenderer.From(() => _ctx.Append($"{accessedObject}.{propertyInfo.Name}")); + DeferredExpressionRenderer typedValueExpressionRenderer = _conversionRenderer.RenderReturnTypeConversion(methodInfo.ReturnType, untypedValueExpressionRenderer); if (methodInfo.ReturnType.ManagedType != KnownManagedType.Void) // getter { - _ctx.AppendLine($"return {accessorExpression};"); + _ctx.Append($"return "); + typedValueExpressionRenderer.Render(); + _ctx.AppendLine(";"); } else // setter { - string valueVarName = _ctx.LocalScope.GetAccessorExpression(methodInfo.Parameters.First(p => !p.IsInjectedInstanceParameter)); - _ctx.AppendLine($"{accessorExpression} = {valueVarName};"); + string valueVarName = _ctx.LocalScope.GetAccessorExpression(methodInfo.Parameters.First(p => !p.IsInjectedInstanceParameter)); // TODO: get rid of IsInjectedInstanceParameter + typedValueExpressionRenderer.Render(); + _ctx.Append(" = ").Append(valueVarName).AppendLine(";"); } } @@ -154,8 +171,8 @@ void RenderMethodParameterList() { if (!isFirst) _ctx.Append(", "); - JSMarshalAsAttributeRenderer marshalAsAttributeRenderer = new(parameterInfo.Type); - _ctx.Append(marshalAsAttributeRenderer.RenderParameterAttribute().NormalizeWhitespace().ToFullString()) + JSMarshalAsAttributeRenderer marshalAsAttributeRenderer = new(); + _ctx.Append(marshalAsAttributeRenderer.RenderParameterAttribute(parameterInfo.Type.JSTypeSyntax).NormalizeWhitespace().ToFullString()) .Append(' ') .Append(parameterInfo.Type.CSharpInteropTypeSyntax) .Append(' ') @@ -165,44 +182,6 @@ void RenderMethodParameterList() } } - private void RenderUserMethodInvocation(MethodInfo methodInfo) - { - // Handle Task return conversion for conversion requiring types - if (methodInfo.ReturnType is { IsNullableType: true, TypeArgument.IsTaskType: true, TypeArgument.RequiresTypeConversion: true }) - { - string convertedTaskExpression = _conversionRenderer.RenderNullableTaskTypeConversion(methodInfo.ReturnType.AsInteropTypeInfo(), "retVal", GetInvocationExpression()); - _ctx.Append("return ").Append(convertedTaskExpression).AppendLine(";"); - } - else if (methodInfo.ReturnType is { IsTaskType: true, TypeArgument.RequiresTypeConversion: true }) - { - string convertedTaskExpression = _conversionRenderer.RenderTaskTypeConversion(methodInfo.ReturnType.AsInteropTypeInfo(), "retVal", GetInvocationExpression()); - _ctx.Append("return ").Append(convertedTaskExpression).AppendLine(";"); - } - else // direct return handling or void invocations - { - if (methodInfo.ReturnType.ManagedType != KnownManagedType.Void) - { - _ctx.Append("return "); - } - _ctx.Append(GetInvocationExpression()) - .AppendLine(";"); - } - - string GetInvocationExpression() - { - if (!methodInfo.IsStatic) - { - MethodParameterInfo instanceParam = methodInfo.Parameters.ElementAt(0); - List memberParams = [.. methodInfo.Parameters.Skip(1)]; - return $"{_ctx.LocalScope.GetAccessorExpression(instanceParam)}.{methodInfo.Name}({string.Join(", ", memberParams.Select(_ctx.LocalScope.GetAccessorExpression))})"; - } - else - { - return $"{_ctx.Class.Name}.{methodInfo.Name}({string.Join(", ", methodInfo.Parameters.Select(_ctx.LocalScope.GetAccessorExpression))})"; - } - } - } - internal void RenderFromObjectMapper() { _ctx.AppendLine($"public static {_ctx.Class.Type.CSharpTypeSyntax} {RenderConstants.FromObject}(object obj)"); @@ -240,7 +219,8 @@ internal void RenderFromJSObjectMapper(ConstructorInfo constructorInfo) private void RenderConstructorInvocation(ConstructorInfo constructorInfo) { PropertyInfo[] propertiesInMapper = [.. constructorInfo.MemberInitializers]; - Dictionary propertyToConvertedVarDict = RenderNonInlinableTypeConversions(propertiesInMapper); + Dictionary propertyToAccessorDict = RenderJSObjectPropertyRetrievalWithTypeConversions(propertiesInMapper); + Debug.Assert(propertyToAccessorDict.Count == propertiesInMapper.Length, "Property count differs from renderer count"); _ctx.Append("return new ").Append(constructorInfo.Type.CSharpTypeSyntax).Append('('); bool isFirst = true; @@ -261,72 +241,55 @@ private void RenderConstructorInvocation(ConstructorInfo constructorInfo) _ctx.AppendLine("{"); using (_ctx.Indent()) { - foreach (PropertyInfo propertyInfo in propertiesInMapper) + foreach ((PropertyInfo propertyInfo, DeferredExpressionRenderer expressionRenderer) in propertyToAccessorDict) { - if (propertyToConvertedVarDict.TryGetValue(propertyInfo, out TypeConversionExpressionRenderDelegate? expressionRenderer)) - { - _ctx.Append($"{propertyInfo.Name} = "); - expressionRenderer.Render(); - } - else - { - _ctx.Append($"{propertyInfo.Name} = "); - string propertyRetrievalExpression = $"jsObject.{JSObjectMethodResolver.ResolveJSObjectMethodName(propertyInfo.Type)}(\"{propertyInfo.Name}\")"; - if (propertyInfo.Type is { IsNullableType: false }) - { - propertyRetrievalExpression = $"({propertyRetrievalExpression} ?? throw new ArgumentException(\"Non-nullable property '{propertyInfo.Name}' missing or of invalid type\", nameof(jsObject)))"; - } - - if (propertyInfo.Type.RequiresTypeConversion) - _conversionRenderer.RenderInlineTypeConversion(propertyInfo.Type, propertyRetrievalExpression); - else - _ctx.Append(propertyRetrievalExpression); - } + _ctx.Append(propertyInfo.Name).Append(" = "); + expressionRenderer.Render(); _ctx.AppendLine(","); } } _ctx.AppendLine("};"); - Dictionary RenderNonInlinableTypeConversions(PropertyInfo[] properties) + Dictionary RenderJSObjectPropertyRetrievalWithTypeConversions(PropertyInfo[] properties) { - Dictionary convertedTaskExpressionDict = []; + Dictionary convertedTaskExpressionDict = []; foreach (PropertyInfo propertyInfo in properties) { - if (propertyInfo.Type is { IsNullableType: true, TypeArgument.IsTaskType: true }) - { - string tmpVarName = $"{propertyInfo.Name}Tmp"; - _ctx.AppendLine($"var {tmpVarName} = jsObject.{JSObjectMethodResolver.ResolveJSObjectMethodName(propertyInfo.Type)}(\"{propertyInfo.Name}\");"); - string convertedTaskExpression = _conversionRenderer.RenderNullableTaskTypeConversion(propertyInfo.Type, propertyInfo.Name, tmpVarName); - convertedTaskExpressionDict.Add(propertyInfo, new TypeConversionExpressionRenderDelegate(() => _ctx.Append(convertedTaskExpression))); - } - else if (propertyInfo.Type is { IsTaskType: true, RequiresTypeConversion: true }) + DeferredExpressionRenderer valueRetrievalExpressionRenderer = DeferredExpressionRenderer.From(() => { + _ctx.Append("jsObject.").Append(_methodResolver.ResolveJSObjectMethodName(propertyInfo.Type)) + .Append("(\"").Append(propertyInfo.Name).Append("\")"); + if (!propertyInfo.Type.IsNullableType) + { + _ctx.Append(" ?? throw new ArgumentException(\"Non-nullable property '") + .Append(propertyInfo.Name) + .Append("' missing or of invalid type\", nameof(jsObject))"); + } + }); + + if (!propertyInfo.Type.RequiresTypeConversion) { - string tmpVarName = $"{propertyInfo.Name}Tmp"; - _ctx.Append($"var {tmpVarName} = jsObject.{JSObjectMethodResolver.ResolveJSObjectMethodName(propertyInfo.Type)}(\"{propertyInfo.Name}\")") - .Append($" ?? throw new ArgumentException(\"Non-nullable property '{propertyInfo.Name}' missing or of invalid type\", nameof(jsObject))") - .AppendLine(";"); - string convertedTaskExpression = _conversionRenderer.RenderTaskTypeConversion(propertyInfo.Type, propertyInfo.Name, tmpVarName); - convertedTaskExpressionDict.Add(propertyInfo, new TypeConversionExpressionRenderDelegate(() => _ctx.Append(convertedTaskExpression))); - } - else if (propertyInfo.Type is { IsNullableType: true, RequiresTypeConversion: true }) + convertedTaskExpressionDict.Add(propertyInfo, valueRetrievalExpressionRenderer); + + } + else { - string tmpVarName = $"{propertyInfo.Name}Tmp"; - _ctx.AppendLine($"var {tmpVarName} = jsObject.{JSObjectMethodResolver.ResolveJSObjectMethodName(propertyInfo.Type)}(\"{propertyInfo.Name}\");"); - convertedTaskExpressionDict.Add(propertyInfo, new TypeConversionExpressionRenderDelegate(() => _conversionRenderer.RenderInlineTypeConversion(propertyInfo.Type, tmpVarName))); + if (propertyInfo.Type.IsDelegateType()) + { + // delegates with conversion requirements need to be stored in a temporary variable to avoid multiple invocations of the JSObject method from the wrapper delegate + _ctx.Append(propertyInfo.Type.CSharpInteropTypeSyntax).Append(" tmp").Append(propertyInfo.Name).Append(" = "); + valueRetrievalExpressionRenderer.Render(); + _ctx.AppendLine(";"); + valueRetrievalExpressionRenderer = DeferredExpressionRenderer.From(() => { + _ctx.Append("tmp").Append(propertyInfo.Name); + }); + } + + DeferredExpressionRenderer convertedValueAccessorRenderer = _conversionRenderer.RenderVarTypeConversion(propertyInfo.Type, propertyInfo.Name, valueRetrievalExpressionRenderer); + convertedTaskExpressionDict.Add(propertyInfo, convertedValueAccessorRenderer); } } return convertedTaskExpressionDict; } } - - private class TypeConversionExpressionRenderDelegate(Action renderAction) - { - internal void Render() => renderAction(); - - public static implicit operator TypeConversionExpressionRenderDelegate(Action renderAction) - { - return new TypeConversionExpressionRenderDelegate(renderAction); - } - } } diff --git a/TypeShim.Generator/CSharp/CSharpTypeConversionRenderer.cs b/TypeShim.Generator/CSharp/CSharpTypeConversionRenderer.cs index 30279e61..681d9873 100644 --- a/TypeShim.Generator/CSharp/CSharpTypeConversionRenderer.cs +++ b/TypeShim.Generator/CSharp/CSharpTypeConversionRenderer.cs @@ -1,4 +1,6 @@ -using System.Diagnostics; +using Microsoft.CodeAnalysis; +using System.Diagnostics; +using System.Reflection; using TypeShim.Generator.Parsing; using TypeShim.Shared; @@ -13,58 +15,118 @@ internal void RenderParameterTypeConversion(MethodParameterInfo parameterInfo) string varName = _ctx.LocalScope.GetAccessorExpression(parameterInfo); string newVarName = $"typed_{_ctx.LocalScope.GetAccessorExpression(parameterInfo)}"; - // task pattern differs from other conversions, hence their fully separated rendering. - if (parameterInfo.Type is { IsNullableType: true, TypeArgument.IsTaskType: true }) // Task? + + DeferredExpressionRenderer convertedValueAccessor = RenderTypeDownConversion(parameterInfo.Type, varName, DeferredExpressionRenderer.From(() => _ctx.Append(varName)), parameterInfo.IsInjectedInstanceParameter); + _ctx.Append($"{parameterInfo.Type.CSharpTypeSyntax} {newVarName} = "); + convertedValueAccessor.Render(); + _ctx.AppendLine(";"); + _ctx.LocalScope.UpdateAccessorExpression(parameterInfo, newVarName); + } + + internal DeferredExpressionRenderer RenderVarTypeConversion(InteropTypeInfo typeInfo, string varName, DeferredExpressionRenderer valueExpressionRenderer) + { + return RenderTypeDownConversion(typeInfo, varName, valueExpressionRenderer, false); + } + + private DeferredExpressionRenderer RenderTypeDownConversion(InteropTypeInfo typeInfo, string accessorName, DeferredExpressionRenderer accessorExpressionRenderer, bool isInstanceParameter) + { + if (!typeInfo.RequiresTypeConversion) + return accessorExpressionRenderer; + + if (typeInfo is { IsNullableType: true, TypeArgument.IsTaskType: true }) // Task? { - string convertedTaskExpression = RenderNullableTaskTypeConversion(parameterInfo.Type, varName, varName); - _ctx.AppendLine($"{parameterInfo.Type.CSharpTypeSyntax} {newVarName} = {convertedTaskExpression};"); + string convertedTaskExpression = RenderNullableTaskTypeConversion(typeInfo, accessorName, accessorExpressionRenderer); + return DeferredExpressionRenderer.From(() => _ctx.Append(convertedTaskExpression)); } - else if (parameterInfo.Type.IsTaskType) // Task + else if (typeInfo.IsTaskType) // Task { - string convertedTaskExpression = RenderTaskTypeConversion(parameterInfo.Type, varName, varName); - _ctx.AppendLine($"{parameterInfo.Type.CSharpTypeSyntax} {newVarName} = {convertedTaskExpression};"); + string convertedTaskExpression = RenderTaskTypeConversion(typeInfo, accessorName, accessorExpressionRenderer); + return DeferredExpressionRenderer.From(() => _ctx.Append(convertedTaskExpression)); } else { - _ctx.Append($"{parameterInfo.Type.CSharpTypeSyntax} {newVarName} = "); - RenderInlineTypeConversion(parameterInfo.Type, varName, forceCovariantConversion: parameterInfo.IsInjectedInstanceParameter); - _ctx.AppendLine(";"); + return DeferredExpressionRenderer.From(() => RenderInlineTypeDownConversion(typeInfo, accessorName, accessorExpressionRenderer, forceCovariantConversion: isInstanceParameter)); + } + } + + /// + /// Renders any lines that may be required to convert the return type from interop to managed. Then returns a delegate to render the expression to access the converted value. + /// + /// + /// + /// + internal DeferredExpressionRenderer RenderReturnTypeConversion(InteropTypeInfo returnType, DeferredExpressionRenderer valueExpressionRenderer) + { + if (!returnType.RequiresTypeConversion) + return valueExpressionRenderer; // DeferredExpressionRenderer.From(() => _ctx.Append(valueExpression)); + + if (returnType is { IsTaskType: true, TypeArgument.RequiresTypeConversion: true }) // Handle Task + { + string convertedValueExpression = RenderTaskTypeConversion(returnType.AsInteropTypeInfo(), "retVal", valueExpressionRenderer); + return DeferredExpressionRenderer.From(() => _ctx.Append(convertedValueExpression)); } + else if (returnType is { IsNullableType: true, TypeArgument.IsTaskType: true, TypeArgument.RequiresTypeConversion: true }) // Handle Task? + { + string convertedValueExpression = RenderNullableTaskTypeConversion(returnType.AsInteropTypeInfo(), "retVal", valueExpressionRenderer); + return DeferredExpressionRenderer.From(() => _ctx.Append(convertedValueExpression)); - _ctx.LocalScope.UpdateAccessorExpression(parameterInfo, newVarName); // TODO: let methodctx decide naming + } + else if (returnType.IsDelegateType() && returnType.ArgumentInfo is DelegateArgumentInfo argumentInfo) // Action/Action/Func + { + // Note: for delegates its important that we store retVal first, to avoid multiple evaluations of valueExpression inside the wrapper delegate, as it can be a method call + _ctx.Append(returnType.CSharpTypeSyntax).Append(" retVal = "); + valueExpressionRenderer.Render(); + _ctx.AppendLine(";"); + return DeferredExpressionRenderer.From(() => RenderInlineDelegateTypeUpConversion(returnType, "retVal", argumentInfo)); + } + else + { + return DeferredExpressionRenderer.From(() => RenderInlineCovariantTypeUpConversion(returnType, valueExpressionRenderer)); + } } - internal void RenderInlineTypeConversion(InteropTypeInfo typeInfo, string varName, bool forceCovariantConversion = false) + private void RenderInlineTypeDownConversion(InteropTypeInfo typeInfo, string accessorName, DeferredExpressionRenderer accessorExpressionRenderer, bool forceCovariantConversion = false) { + ArgumentNullException.ThrowIfNull(typeInfo, nameof(typeInfo)); if (forceCovariantConversion) { - RenderInlineCovariantTypeConversion(typeInfo, varName); + RenderInlineCovariantTypeDownConversion(typeInfo, accessorExpressionRenderer); } else if (typeInfo.IsArrayType) { - RenderInlineArrayTypeConversion(typeInfo, varName); + RenderInlineArrayTypeDownConversion(typeInfo, accessorExpressionRenderer); } else if (typeInfo.IsNullableType) { - RenderInlineNullableTypeConversion(typeInfo, varName); + RenderInlineNullableTypeDownConversion(typeInfo, accessorName, accessorExpressionRenderer); } else if (typeInfo.ManagedType is KnownManagedType.Object) { - RenderInlineObjectTypeConversion(typeInfo, varName); + RenderInlineObjectTypeDownConversion(typeInfo, accessorExpressionRenderer); + } + else if (typeInfo.IsDelegateType() && typeInfo.ArgumentInfo is DelegateArgumentInfo argumentInfo) // Action/Action/Func + { + RenderInlineDelegateTypeDownConversion(argumentInfo, accessorName, accessorExpressionRenderer); } - else // Tests guard against this case. Anyway, here is a state-of-the-art regression detector. + else { throw new NotImplementedException($"Type conversion not implemented for type: {typeInfo.CSharpTypeSyntax}. Please file an issue at https://github.com/ArcadeMode/TypeShim"); } } - private void RenderInlineCovariantTypeConversion(InteropTypeInfo typeInfo, string parameterName) + private void RenderInlineCovariantTypeDownConversion(InteropTypeInfo typeInfo, DeferredExpressionRenderer accessorExpressionRenderer) { - Debug.Assert(typeInfo.ManagedType is KnownManagedType.Object or KnownManagedType.Array, "Unexpected non-object or non-array type with required type conversion"); - _ctx.Append($"({typeInfo.CSharpTypeSyntax}){parameterName}"); + _ctx.Append($"({typeInfo.CSharpTypeSyntax})"); + accessorExpressionRenderer.Render(); + } + + private void RenderInlineCovariantTypeUpConversion(InteropTypeInfo typeInfo, DeferredExpressionRenderer valueExpressionRenderer) + { + _ctx.Append($"({typeInfo.CSharpInteropTypeSyntax})"); + valueExpressionRenderer.Render(); } - private void RenderInlineObjectTypeConversion(InteropTypeInfo typeInfo, string parameterName) + private void RenderInlineObjectTypeDownConversion(InteropTypeInfo typeInfo, DeferredExpressionRenderer accessorExpressionRenderer) { Debug.Assert(typeInfo.ManagedType == KnownManagedType.Object, "Attempting object type conversion with non-object"); @@ -72,37 +134,43 @@ private void RenderInlineObjectTypeConversion(InteropTypeInfo typeInfo, string p { ClassInfo exportedClass = _ctx.SymbolMap.GetClassInfo(typeInfo); string targetInteropClass = RenderConstants.InteropClassName(exportedClass); - _ctx.Append($"{targetInteropClass}.{RenderConstants.FromObject}({parameterName})"); + _ctx.Append($"{targetInteropClass}.{RenderConstants.FromObject}("); + accessorExpressionRenderer.Render(); + _ctx.Append(")"); } else { - RenderInlineCovariantTypeConversion(typeInfo, parameterName); + RenderInlineCovariantTypeDownConversion(typeInfo, accessorExpressionRenderer); } } - private void RenderInlineNullableTypeConversion(InteropTypeInfo typeInfo, string parameterName) + private void RenderInlineNullableTypeDownConversion(InteropTypeInfo typeInfo, string accessorName, DeferredExpressionRenderer accessorExpressionRenderer) { Debug.Assert(typeInfo.IsNullableType, "Type must be nullable for nullable type conversion."); Debug.Assert(typeInfo.TypeArgument != null, "Nullable type must have a type argument."); - _ctx.Append($"{parameterName} != null ? "); - RenderInlineTypeConversion(typeInfo.TypeArgument, parameterName); + accessorExpressionRenderer.Render(); + _ctx.Append(" is { } ") + .Append(accessorName).Append("Val") + .Append(" ? "); + RenderInlineTypeDownConversion(typeInfo.TypeArgument, accessorName, DeferredExpressionRenderer.From(() => _ctx.Append(accessorName).Append("Val"))); _ctx.Append(" : null"); } - private void RenderInlineArrayTypeConversion(InteropTypeInfo typeInfo, string parameterName) + private void RenderInlineArrayTypeDownConversion(InteropTypeInfo typeInfo, DeferredExpressionRenderer accessorExpressionRenderer) { Debug.Assert(typeInfo.TypeArgument != null, "Array type must have a type argument."); - InteropTypeInfo elementTypeInfo = typeInfo.TypeArgument ?? throw new InvalidOperationException("Array type must have a type argument for conversion."); if (typeInfo.TypeArgument.IsTSExport == false) { - RenderInlineCovariantTypeConversion(typeInfo, parameterName); + RenderInlineCovariantTypeDownConversion(typeInfo, accessorExpressionRenderer); // no special conversion possible for non-exported types } else { - _ctx.Append($"Array.ConvertAll({parameterName}, e => "); - RenderInlineTypeConversion(typeInfo.TypeArgument, "e"); + _ctx.Append("Array.ConvertAll("); + accessorExpressionRenderer.Render(); + _ctx.Append(", e => "); + RenderInlineTypeDownConversion(typeInfo.TypeArgument, "e", DeferredExpressionRenderer.From(() => _ctx.Append("e"))); _ctx.Append(')'); } } @@ -111,42 +179,125 @@ private void RenderInlineArrayTypeConversion(InteropTypeInfo typeInfo, string pa /// returns an expression to access the converted task with. /// /// - internal string RenderTaskTypeConversion(InteropTypeInfo targetTaskType, string sourceVarName, string sourceTaskExpression) + private string RenderTaskTypeConversion(InteropTypeInfo targetTaskType, string sourceVarName, DeferredExpressionRenderer sourceTaskExpressionRenderer) { InteropTypeInfo taskTypeParamInfo = targetTaskType.TypeArgument ?? throw new InvalidOperationException("Task type must have a type argument for conversion."); string tcsVarName = $"{sourceVarName}Tcs"; - _ctx.AppendLine($"TaskCompletionSource<{taskTypeParamInfo.CSharpTypeSyntax}> {tcsVarName} = new();") - .AppendLine($"{sourceTaskExpression}.ContinueWith(t => {{"); + _ctx.AppendLine($"TaskCompletionSource<{taskTypeParamInfo.CSharpTypeSyntax}> {tcsVarName} = new();"); + + _ctx.Append('('); + sourceTaskExpressionRenderer.Render(); + _ctx.AppendLine(").ContinueWith(t => {"); + using (_ctx.Indent()) { _ctx.AppendLine($"if (t.IsFaulted) {tcsVarName}.SetException(t.Exception.InnerExceptions);"); _ctx.AppendLine($"else if (t.IsCanceled) {tcsVarName}.SetCanceled();"); _ctx.Append($"else {tcsVarName}.SetResult("); - RenderInlineTypeConversion(taskTypeParamInfo, "t.Result"); + RenderInlineTypeDownConversion(taskTypeParamInfo, tcsVarName, DeferredExpressionRenderer.From(() => _ctx.Append("t.Result"))); _ctx.AppendLine(");"); } _ctx.AppendLine("}, TaskContinuationOptions.ExecuteSynchronously);"); return $"{tcsVarName}.Task"; } - internal string RenderNullableTaskTypeConversion(InteropTypeInfo targetNullableTaskType, string sourceVarName, string sourceTaskExpression) + private string RenderNullableTaskTypeConversion(InteropTypeInfo targetNullableTaskType, string sourceVarName, DeferredExpressionRenderer sourceTaskExpressionRenderer) { InteropTypeInfo taskTypeParamInfo = targetNullableTaskType.TypeArgument ?? throw new InvalidOperationException("Nullable type must have a type argument for conversion."); InteropTypeInfo taskReturnTypeParamInfo = taskTypeParamInfo.TypeArgument ?? throw new InvalidOperationException("Task type must have a type argument for conversion."); + string tcsVarName = $"{sourceVarName}Tcs"; - _ctx.AppendLine($"TaskCompletionSource<{taskReturnTypeParamInfo.CSharpTypeSyntax}>? {tcsVarName} = {sourceTaskExpression} != null ? new() : null;"); - _ctx.AppendLine($"{sourceTaskExpression}?.ContinueWith(t => {{"); + _ctx.Append("TaskCompletionSource<").Append(taskReturnTypeParamInfo.CSharpTypeSyntax.ToString()).Append(">? ").Append(tcsVarName).Append(" = "); + sourceTaskExpressionRenderer.Render(); + _ctx.AppendLine(" != null ? new() : null;"); + sourceTaskExpressionRenderer.Render(); + _ctx.AppendLine("?.ContinueWith(t => {"); using (_ctx.Indent()) { _ctx.AppendLine($"if (t.IsFaulted) {tcsVarName}!.SetException(t.Exception.InnerExceptions);") .AppendLine($"else if (t.IsCanceled) {tcsVarName}!.SetCanceled();"); _ctx.Append($"else {tcsVarName}!.SetResult("); - RenderInlineTypeConversion(taskReturnTypeParamInfo, "t.Result"); + RenderInlineTypeDownConversion(taskReturnTypeParamInfo, tcsVarName, DeferredExpressionRenderer.From(() => _ctx.Append("t.Result"))); _ctx.AppendLine(");"); } _ctx.AppendLine("}, TaskContinuationOptions.ExecuteSynchronously);"); return $"{tcsVarName}?.Task"; } + + private void RenderInlineDelegateTypeDownConversion(DelegateArgumentInfo argumentInfo, string accessorName, DeferredExpressionRenderer accessorExpressionRenderer) + { + _ctx.Append('('); + for (int i = 0; i < argumentInfo.ParameterTypes.Length; i++) + { + if (i > 0) _ctx.Append(", "); + _ctx.Append(argumentInfo.ParameterTypes[i].CSharpTypeSyntax).Append(' ').Append("arg").Append(i); + } + _ctx.Append(") => "); + + DeferredExpressionRenderer invocationExpressionRenderer = DeferredExpressionRenderer.From(() => + { + accessorExpressionRenderer.Render(); + _ctx.Append('('); + for (int i = 0; i < argumentInfo.ParameterTypes.Length; i++) + { + if (i > 0) _ctx.Append(", "); + // in body of upcasted delegate, to invoke original delegate we simply pass to downcast the parameter types + _ctx.Append("arg").Append(i); + } + _ctx.Append(')'); + }); + + if (argumentInfo.ReturnType.RequiresTypeConversion) + { + RenderInlineTypeDownConversion(argumentInfo.ReturnType, accessorName, invocationExpressionRenderer); + } + else + { + invocationExpressionRenderer.Render(); + } + } + + private void RenderInlineDelegateTypeUpConversion(InteropTypeInfo typeInfo, string varName, DelegateArgumentInfo argumentInfo) + { + _ctx.Append('('); + for (int i = 0; i < argumentInfo.ParameterTypes.Length; i++) + { + if (i > 0) + _ctx.Append(", "); + _ctx.Append(argumentInfo.ParameterTypes[i].CSharpInteropTypeSyntax).Append(' ').Append("arg").Append(i); + } + _ctx.Append(") => "); + + DeferredExpressionRenderer invocationExpressionRenderer = DeferredExpressionRenderer.From(() => + { + _ctx.Append(varName).Append('('); + for (int i = 0; i < argumentInfo.ParameterTypes.Length; i++) + { + if (i > 0) _ctx.Append(", "); + + // in body of downcasted delegate, to invoke original delegate we must upcast the types again + DeferredExpressionRenderer argNameRenderer = DeferredExpressionRenderer.From(() => _ctx.Append("arg").Append(i)); + if (argumentInfo.ParameterTypes[i].RequiresTypeConversion) + { + RenderInlineTypeDownConversion(argumentInfo.ParameterTypes[i], $"arg{i}", argNameRenderer); // TODO: refactor delegatearginfo to contain methodparameterinfo for names + } + else + { + argNameRenderer.Render(); + } + } + _ctx.Append(')'); + }); + + if (argumentInfo.ReturnType.RequiresTypeConversion) + { + RenderInlineCovariantTypeUpConversion(argumentInfo.ReturnType, invocationExpressionRenderer); + } + else + { + invocationExpressionRenderer.Render(); + } + } } \ No newline at end of file diff --git a/TypeShim.Generator/CSharp/DeferredExpressionRenderer.cs b/TypeShim.Generator/CSharp/DeferredExpressionRenderer.cs new file mode 100644 index 00000000..640eb603 --- /dev/null +++ b/TypeShim.Generator/CSharp/DeferredExpressionRenderer.cs @@ -0,0 +1,13 @@ +namespace TypeShim.Generator.CSharp; + +internal class DeferredExpressionRenderer(Action renderAction) +{ + internal void Render() => renderAction(); + + public static implicit operator DeferredExpressionRenderer(Action renderAction) + { + return new DeferredExpressionRenderer(renderAction); + } + + public static DeferredExpressionRenderer From(Action renderAction) => new(renderAction); +} diff --git a/TypeShim.Generator/CSharp/JSMarshalAsAttributeRenderer.cs b/TypeShim.Generator/CSharp/JSMarshalAsAttributeRenderer.cs index aaf54408..ea3823e5 100644 --- a/TypeShim.Generator/CSharp/JSMarshalAsAttributeRenderer.cs +++ b/TypeShim.Generator/CSharp/JSMarshalAsAttributeRenderer.cs @@ -5,7 +5,7 @@ namespace TypeShim.Generator.CSharp; -internal sealed class JSMarshalAsAttributeRenderer(InteropTypeInfo interopTypeInfo) +internal sealed class JSMarshalAsAttributeRenderer() { internal AttributeListSyntax RenderJSExportAttribute() { @@ -17,25 +17,44 @@ internal AttributeListSyntax RenderJSExportAttribute() ) ); } + + internal AttributeListSyntax RenderJSImportAttribute(string method) + { + return SyntaxFactory.AttributeList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Attribute( + SyntaxFactory.IdentifierName("JSImport"), + SyntaxFactory.AttributeArgumentList([ + SyntaxFactory.AttributeArgument( + SyntaxFactory.ParseExpression($"\"{method}\"") + ), + SyntaxFactory.AttributeArgument( + SyntaxFactory.ParseExpression("\"@typeshim\"") + ) + ]) + ) + ) + ); + } - internal AttributeListSyntax RenderReturnAttribute() + internal AttributeListSyntax RenderReturnAttribute(TypeSyntax jsTypeSyntax) { - return RenderAttributeListWithJSMarshalAs().WithTarget( // 'return:' + return RenderAttributeListWithJSMarshalAs(jsTypeSyntax).WithTarget( // 'return:' SyntaxFactory.AttributeTargetSpecifier( SyntaxFactory.Token(SyntaxKind.ReturnKeyword) ) ); } - internal AttributeListSyntax RenderParameterAttribute() + internal AttributeListSyntax RenderParameterAttribute(TypeSyntax jsTypeSyntax) { - return RenderAttributeListWithJSMarshalAs(); + return RenderAttributeListWithJSMarshalAs(jsTypeSyntax); } - private AttributeListSyntax RenderAttributeListWithJSMarshalAs() + private AttributeListSyntax RenderAttributeListWithJSMarshalAs(TypeSyntax jsTypeSyntax) { TypeArgumentListSyntax marshalAsTypeArgument = SyntaxFactory.TypeArgumentList( - SyntaxFactory.SingletonSeparatedList(interopTypeInfo.JSTypeSyntax) + SyntaxFactory.SingletonSeparatedList(jsTypeSyntax) ); AttributeSyntax marshalAsAttribute = SyntaxFactory.Attribute(SyntaxFactory.GenericName("JSMarshalAs").WithTypeArgumentList(marshalAsTypeArgument)); diff --git a/TypeShim.Generator/CSharp/JSObjectExtensionInfo.cs b/TypeShim.Generator/CSharp/JSObjectExtensionInfo.cs new file mode 100644 index 00000000..d9a0f51b --- /dev/null +++ b/TypeShim.Generator/CSharp/JSObjectExtensionInfo.cs @@ -0,0 +1,42 @@ +using TypeShim.Shared; + +namespace TypeShim.Generator.CSharp; + +internal record JSObjectExtensionInfo(InteropTypeInfo TypeInfo) +{ + internal string Name = string.Join("", GetManagedTypeListForType(TypeInfo)); + + internal string GetMarshalAsMethodName() + { + return $"MarshalAs{Name}"; + } + + internal string GetGetPropertyAsMethodName() + { + return $"GetPropertyAs{Name}Nullable"; + } + + private static IEnumerable GetManagedTypeListForType(InteropTypeInfo typeInfo) + { + IEnumerable managedTypes = []; + BuildManagedTypeEnumerableRecursive(); + return managedTypes; + + void BuildManagedTypeEnumerableRecursive() + { + if (typeInfo.IsDelegateType() && typeInfo.ArgumentInfo is DelegateArgumentInfo delegateArgInfo) + { + foreach (InteropTypeInfo paramType in delegateArgInfo.ParameterTypes) + { + managedTypes = managedTypes.Concat(GetManagedTypeListForType(paramType)); + } + managedTypes = managedTypes.Concat(GetManagedTypeListForType(delegateArgInfo.ReturnType)); + } + else if (typeInfo.TypeArgument != null) + { + managedTypes = managedTypes.Concat(GetManagedTypeListForType(typeInfo.TypeArgument)); + } + managedTypes = managedTypes.Append(typeInfo.ManagedType); + } + } +} diff --git a/TypeShim.Generator/CSharp/JSObjectExtensionsRenderer.cs b/TypeShim.Generator/CSharp/JSObjectExtensionsRenderer.cs index 4a8adb29..aa40131a 100644 --- a/TypeShim.Generator/CSharp/JSObjectExtensionsRenderer.cs +++ b/TypeShim.Generator/CSharp/JSObjectExtensionsRenderer.cs @@ -1,310 +1,73 @@ -using System; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; using System.Collections.Generic; using System.Reflection; using System.Runtime.InteropServices.JavaScript; using System.Text; using TypeShim.Generator.Parsing; +using TypeShim.Shared; namespace TypeShim.Generator.CSharp; -internal sealed class JSObjectExtensionsRenderer() +internal sealed class JSObjectExtensionsRenderer(RenderContext _ctx, IEnumerable targetTypeInfos) { - private readonly StringBuilder sb = new(); - public string Render() - { - sb.AppendLine("#nullable enable") - .AppendLine("// JSImports for the type marshalling process") - .AppendLine("using System;") - .AppendLine("using System.Runtime.InteropServices.JavaScript;") - .AppendLine("using System.Threading.Tasks;"); - - // raison d'etre: type mapping limitations: https://learn.microsoft.com/en-us/aspnet/core/client-side/dotnet-interop/?view=aspnetcore-10.0#type-mapping-limitations - // 1. JSObject has no means to retrieve arrays beside ByteArray (automapping user classes with an array property type is therefore not possible by default) - // 2. Nested types cannot be represented on the interop boundary (i.e. Task - - sb.AppendLine(JSObjectIntExtensionsClass); - sb.AppendLine(JSObjectArrayExtensionsClass); - sb.AppendLine(JSObjectTaskExtensionsClass); - //TODO: Consider targeting different moniker and provide this class through TypeShim nuget so the user can utilize these directly if they so wish. - return sb.ToString(); - } - - private const string JSObjectIntExtensionsClass = """ -public static partial class JSObjectIntExtensions -{ - public static byte? GetPropertyAsByteNullable(this JSObject jsObject, string propertyName) - { - return jsObject.HasProperty(propertyName) ? (byte)jsObject.GetPropertyAsInt32(propertyName) : null; - } - - public static short? GetPropertyAsInt16Nullable(this JSObject jsObject, string propertyName) - { - return jsObject.HasProperty(propertyName) ? (short)jsObject.GetPropertyAsInt32(propertyName) : null; - } - - public static int? GetPropertyAsInt32Nullable(this JSObject jsObject, string propertyName) - { - return jsObject.HasProperty(propertyName) ? jsObject.GetPropertyAsInt32(propertyName) : null; - } - - public static long? GetPropertyAsInt64Nullable(this JSObject jsObject, string propertyName) - { - return jsObject.HasProperty(propertyName) ? (long)jsObject.GetPropertyAsInt32(propertyName) : null; - } - - public static nint? GetPropertyAsIntPtrNullable(this JSObject jsObject, string propertyName) - { - return jsObject.HasProperty(propertyName) ? (nint)jsObject.GetPropertyAsInt32(propertyName) : null; - } - - public static bool? GetPropertyAsBooleanNullable(this JSObject jsObject, string propertyName) - { - return jsObject.HasProperty(propertyName) ? jsObject.GetPropertyAsBoolean(propertyName) : null; - } - - public static double? GetPropertyAsDoubleNullable(this JSObject jsObject, string propertyName) - { - return jsObject.HasProperty(propertyName) ? jsObject.GetPropertyAsDouble(propertyName) : null; - } - - public static float? GetPropertyAsFloatNullable(this JSObject jsObject, string propertyName) - { - return jsObject.HasProperty(propertyName) ? (float)jsObject.GetPropertyAsDouble(propertyName) : null; - } - - public static char? GetPropertyAsCharNullable(this JSObject jsObject, string propertyName) - { - return jsObject.HasProperty(propertyName) ? char.Parse(jsObject.GetPropertyAsString(propertyName)!) : null; - } - - public static DateTime? GetPropertyAsDateTimeNullable(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsDateTime(value) : null; - } - - public static DateTimeOffset? GetPropertyAsDateTimeOffsetNullable(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsDateTimeOffset(value) : null; - } - - public static object? GetPropertyAsObject(this JSObject jsObject, string propertyName) - { - return jsObject.HasProperty(propertyName) ? MarshallPropertyAsObject(jsObject, propertyName) : null; - } - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs] - public static partial DateTime MarshallAsDateTime([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs] - public static partial DateTimeOffset MarshallAsDateTimeOffset([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrapProperty", "@typeshim")] - [return: JSMarshalAs] - public static partial object MarshallPropertyAsObject([JSMarshalAs] JSObject obj, [JSMarshalAs] string propertyName); -} - -"""; - - private const string JSObjectArrayExtensionsClass = """ -public static partial class JSObjectArrayExtensions -{ - public static int[]? GetPropertyAsInt32Array(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsIntArray(value) : null; - } - - public static double[]? GetPropertyAsDoubleArray(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsDoubleArray(value) : null; - } - - public static string[]? GetPropertyAsStringArray(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsStringArray(value) : null; - } - - public static JSObject[]? GetPropertyAsJSObjectArray(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsJSObjectArray(value) : null; - } - - public static object[]? GetPropertyAsObjectArray(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsObjectArray(value) : null; - } - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial int[] MarshallAsIntArray([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial double[] MarshallAsDoubleArray([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial string[] MarshallAsStringArray([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial JSObject[] MarshallAsJSObjectArray([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial object[] MarshallAsObjectArray([JSMarshalAs] JSObject jsObject); -} - -"""; - - private const string JSObjectTaskExtensionsClass = """ -public static partial class JSObjectTaskExtensions -{ - public static Task? GetPropertyAsVoidTask(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsVoidTask(value) : null; - } - - public static Task? GetPropertyAsBooleanTask(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsBooleanTask(value) : null; - } - - public static Task? GetPropertyAsByteTask(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsByteTask(value) : null; - } - - public static Task? GetPropertyAsCharTask(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsCharTask(value) : null; - } - - public static Task? GetPropertyAsInt16Task(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsInt16Task(value) : null; - } - - public static Task? GetPropertyAsInt32Task(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsInt32Task(value) : null; - } - - public static Task? GetPropertyAsInt64Task(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsInt64Task(value) : null; - } - - public static Task? GetPropertyAsSingleTask(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsSingleTask(value) : null; - } - - public static Task? GetPropertyAsDoubleTask(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsDoubleTask(value) : null; - } - - public static Task? GetPropertyAsIntPtrTask(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsIntPtrTask(value) : null; - } - - public static Task? GetPropertyAsDateTimeTask(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsDateTimeTask(value) : null; + public void Render() + { + _ctx.AppendLine("#nullable enable") + .AppendLine("// JSImports for the type marshalling process") + .AppendLine("using System;") + .AppendLine("using System.Runtime.InteropServices.JavaScript;") + .AppendLine("using System.Threading.Tasks;") + .AppendLine("public static partial class JSObjectExtensions") + .AppendLine("{"); + using (_ctx.Indent()) + { + JSObjectExtensionInfo[] extensionInfos = [.. targetTypeInfos + .Select(typeInfo => new JSObjectExtensionInfo(typeInfo)) + .DistinctBy(extInfo => extInfo.Name)]; + HashSet processedTypes = []; + foreach (JSObjectExtensionInfo typeInfo in extensionInfos) + { + RenderExtensionMethodForType(typeInfo); + } + } + _ctx.AppendLine("}"); + } + + private void RenderExtensionMethodForType(JSObjectExtensionInfo extensionInfo) + { + DeferredExpressionRenderer marshalAsMethodNameRenderer = DeferredExpressionRenderer.From(() => + { + _ctx.Append("MarshalAs").Append(extensionInfo.Name); + }); + DeferredExpressionRenderer getPropertyAsMethodNameRenderer = DeferredExpressionRenderer.From(() => + { + _ctx.Append("GetPropertyAs").Append(extensionInfo.Name).Append("Nullable"); + }); + + _ctx.Append("public static ").Append(extensionInfo.TypeInfo.CSharpInteropTypeSyntax).Append("? "); + getPropertyAsMethodNameRenderer.Render(); + _ctx.AppendLine("(this JSObject jsObject, string propertyName)"); + _ctx.AppendLine("{"); + using (_ctx.Indent()) + { + _ctx.Append("return jsObject.HasProperty(propertyName) ? "); + marshalAsMethodNameRenderer.Render(); + _ctx.AppendLine("(jsObject, propertyName) : null;"); + } + _ctx.AppendLine("}"); + + JSMarshalAsAttributeRenderer attributeRenderer = new(); + _ctx.AppendLine(attributeRenderer.RenderJSImportAttribute("unwrapProperty").NormalizeWhitespace().ToString()); + _ctx.AppendLine(attributeRenderer.RenderReturnAttribute(extensionInfo.TypeInfo.JSTypeSyntax).NormalizeWhitespace().ToString()); + _ctx.Append("public static partial ").Append(extensionInfo.TypeInfo.CSharpInteropTypeSyntax).Append(' '); + marshalAsMethodNameRenderer.Render(); + _ctx.Append('(') + .Append(attributeRenderer.RenderParameterAttribute(SyntaxFactory.ParseTypeName("JSType.Object")).NormalizeWhitespace()).Append(' ').Append(InteropTypeInfo.JSObjectTypeInfo.CSharpInteropTypeSyntax).Append(" obj") + .Append(", ") + .Append(attributeRenderer.RenderParameterAttribute(SyntaxFactory.ParseTypeName("JSType.String")).NormalizeWhitespace()).Append(" string propertyName") + .AppendLine(");"); } - - public static Task? GetPropertyAsDateTimeOffsetTask(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsDateTimeOffsetTask(value) : null; - } - - public static Task? GetPropertyAsExceptionTask(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsExceptionTask(value) : null; - } - - public static Task? GetPropertyAsJSObjectTask(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsJSObjectTask(value) : null; - } - - public static Task? GetPropertyAsStringTask(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsStringTask(value) : null; - } - - public static Task? GetPropertyAsObjectTask(this JSObject jsObject, string propertyName) - { - return jsObject.GetPropertyAsJSObject(propertyName) is JSObject value ? MarshallAsObjectTask(value) : null; - } - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsVoidTask([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsBooleanTask([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsByteTask([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrapCharPromise", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsCharTask([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsInt16Task([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsInt32Task([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsInt64Task([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsSingleTask([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsDoubleTask([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsIntPtrTask([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsDateTimeTask([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsDateTimeOffsetTask([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsExceptionTask([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsJSObjectTask([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsStringTask([JSMarshalAs] JSObject jsObject); - - [JSImport("unwrap", "@typeshim")] - [return: JSMarshalAs>] - public static partial Task MarshallAsObjectTask([JSMarshalAs] JSObject jsObject); -} - -"""; } diff --git a/TypeShim.Generator/CSharp/JSObjectMethodResolver.cs b/TypeShim.Generator/CSharp/JSObjectMethodResolver.cs index 1a32d49d..b47a94d3 100644 --- a/TypeShim.Generator/CSharp/JSObjectMethodResolver.cs +++ b/TypeShim.Generator/CSharp/JSObjectMethodResolver.cs @@ -2,75 +2,22 @@ namespace TypeShim.Generator.CSharp; -internal static class JSObjectMethodResolver +internal class JSObjectMethodResolver(List resolvedTypes) { - internal static string ResolveJSObjectMethodName(InteropTypeInfo typeInfo) + internal string ResolveJSObjectMethodName(InteropTypeInfo typeInfo) { - return typeInfo.ManagedType switch + if (typeInfo.ManagedType is KnownManagedType.Nullable) { - KnownManagedType.Nullable => ResolveJSObjectMethodName(typeInfo.TypeArgument!), - KnownManagedType.Boolean => "GetPropertyAsBooleanNullable", - KnownManagedType.Double => "GetPropertyAsDoubleNullable", - KnownManagedType.Single => "GetPropertyAsFloatNullable", - KnownManagedType.Char => "GetPropertyAsCharNullable", - KnownManagedType.String => "GetPropertyAsString", - KnownManagedType.Byte => "GetPropertyAsByteNullable", - KnownManagedType.Int16 => "GetPropertyAsInt16Nullable", - KnownManagedType.Int32 => "GetPropertyAsInt32Nullable", - KnownManagedType.Int64 => "GetPropertyAsInt64Nullable", - KnownManagedType.IntPtr => "GetPropertyAsIntPtrNullable", - KnownManagedType.DateTime => "GetPropertyAsDateTimeNullable", - KnownManagedType.DateTimeOffset => "GetPropertyAsDateTimeOffsetNullable", - KnownManagedType.JSObject => "GetPropertyAsJSObject", - KnownManagedType.Object when typeInfo.IsTSExport => "GetPropertyAsJSObject", // exported object types have a FromJSObject mapper - KnownManagedType.Object when !typeInfo.IsTSExport => "GetPropertyAsObject", // non-exports are just casted to their original type. - KnownManagedType.Array => typeInfo.TypeArgument switch - { - { ManagedType: KnownManagedType.Byte } => "GetPropertyAsByteArray", - { ManagedType: KnownManagedType.Int32 } => "GetPropertyAsInt32Array", - { ManagedType: KnownManagedType.Double } => "GetPropertyAsDoubleArray", - { ManagedType: KnownManagedType.String } => "GetPropertyAsStringArray", - { ManagedType: KnownManagedType.JSObject } => "GetPropertyAsJSObjectArray", - { ManagedType: KnownManagedType.Object, IsTSExport: true } => "GetPropertyAsJSObjectArray", // exported object types have a FromJSObject mapper - { ManagedType: KnownManagedType.Object, IsTSExport: false } => "GetPropertyAsObjectArray", // non-exports are just casted to their original type. - { ManagedType: KnownManagedType.Nullable } elemTypeInfo => elemTypeInfo.TypeArgument switch - { - { ManagedType: KnownManagedType.JSObject } => "GetPropertyAsJSObjectArray", - { ManagedType: KnownManagedType.Object, IsTSExport: true } => "GetPropertyAsJSObjectArray", // exported object types have a FromJSObject mapper - { ManagedType: KnownManagedType.Object, IsTSExport: false } => "GetPropertyAsObjectArray", // non-exports are just casted to their original type. - _ => throw new InvalidOperationException($"Array of nullable type '{elemTypeInfo?.ManagedType}' cannot be marshalled through TypeShim JSObject extensions"), - }, - _ => throw new InvalidOperationException($"Array of type '{typeInfo.TypeArgument?.ManagedType}' cannot be marshalled through TypeShim JSObject extensions"), - }, - KnownManagedType.Task => typeInfo.TypeArgument switch - { - null or { ManagedType: KnownManagedType.Void } => "GetPropertyAsVoidTask", - { ManagedType: KnownManagedType.Boolean } => "GetPropertyAsBooleanTask", - { ManagedType: KnownManagedType.Byte } => "GetPropertyAsByteTask", - { ManagedType: KnownManagedType.Char } => "GetPropertyAsCharTask", - { ManagedType: KnownManagedType.Int16 } => "GetPropertyAsInt16Task", - { ManagedType: KnownManagedType.Int32 } => "GetPropertyAsInt32Task", - { ManagedType: KnownManagedType.Int64 } => "GetPropertyAsInt64Task", - { ManagedType: KnownManagedType.Single } => "GetPropertyAsSingleTask", - { ManagedType: KnownManagedType.Double } => "GetPropertyAsDoubleTask", - { ManagedType: KnownManagedType.IntPtr } => "GetPropertyAsIntPtrTask", - { ManagedType: KnownManagedType.DateTime } => "GetPropertyAsDateTimeTask", - { ManagedType: KnownManagedType.DateTimeOffset } => "GetPropertyAsDateTimeOffsetTask", - { ManagedType: KnownManagedType.Exception } => "GetPropertyAsExceptionTask", - { ManagedType: KnownManagedType.String } => "GetPropertyAsStringTask", - { ManagedType: KnownManagedType.JSObject } => "GetPropertyAsJSObjectTask", - { ManagedType: KnownManagedType.Object, IsTSExport: true } => "GetPropertyAsJSObjectTask", - { ManagedType: KnownManagedType.Object, IsTSExport: false } => "GetPropertyAsObjectTask", - { ManagedType: KnownManagedType.Nullable } returnTypeInfo => returnTypeInfo.TypeArgument switch - { - { ManagedType: KnownManagedType.JSObject } => "GetPropertyAsJSObjectTask", - { ManagedType: KnownManagedType.Object, IsTSExport: true } => "GetPropertyAsJSObjectTask", // exported object types have a FromJSObject mapper - { ManagedType: KnownManagedType.Object, IsTSExport: false } => "GetPropertyAsObjectTask", // exported object types have a FromJSObject mapper - _ => throw new InvalidOperationException($"Task of nullable type '{returnTypeInfo?.ManagedType}' cannot be marshalled through TypeShim JSObject extensions"), - }, - _ => throw new InvalidOperationException($"Task of type '{typeInfo.TypeArgument?.ManagedType}' cannot be marshalled through TypeShim JSObject extensions"), - }, - _ => throw new InvalidOperationException($"Type '{typeInfo.ManagedType}' cannot be marshalled through JSObject nor TypeShim JSObject extensions"), - }; + return ResolveJSObjectMethodName(typeInfo.TypeArgument!); + } + + string extensionMethodName = new JSObjectExtensionInfo(typeInfo).GetGetPropertyAsMethodName(); + resolvedTypes.Add(typeInfo); + return extensionMethodName; + } + + internal IEnumerable GetResolvedTypes() + { + return resolvedTypes; } } \ No newline at end of file diff --git a/TypeShim.Generator/Parsing/ConstructorInfoBuilder.cs b/TypeShim.Generator/Parsing/ConstructorInfoBuilder.cs index 7e62933e..074f96c8 100644 --- a/TypeShim.Generator/Parsing/ConstructorInfoBuilder.cs +++ b/TypeShim.Generator/Parsing/ConstructorInfoBuilder.cs @@ -10,7 +10,6 @@ internal sealed class ConstructorInfoBuilder(INamedTypeSymbol classSymbol, IMeth PropertyInfo[] initializerProperties = [..classProperties.Where(p => p is { SetMethod: { } } or { InitMethod: { } })]; MethodParameterInfo[] parameterInfos = [.. parameterInfoBuilder.Build()]; - // TODO: somehow support optional parameters. For now user must provide all parameters when constructing from JS object. MethodParameterInfo? initializersObjectParameter = initializerProperties.Length == 0 ? null : new() { Name = "jsObject", diff --git a/TypeShim.Generator/Program.cs b/TypeShim.Generator/Program.cs index 8d8e149f..32796e9f 100644 --- a/TypeShim.Generator/Program.cs +++ b/TypeShim.Generator/Program.cs @@ -22,7 +22,7 @@ Task generateCS = Task.Run(() => GenerateCSharpInteropCode(parsedArgs, classInfos)); await Task.WhenAll(generateTS, generateCS); -} +} catch (TypeShimException ex) // known exceptions warrant only an error message { Console.Error.WriteLine($"TypeShim received invalid input, no code was generated. {ex.GetType().Name} {ex.Message}"); @@ -33,16 +33,20 @@ static void GenerateCSharpInteropCode(ProgramArguments parsedArgs, List classInfos) { + List resolvedTypes = []; + JSObjectMethodResolver methodResolver = new(resolvedTypes); + foreach (ClassInfo classInfo in classInfos) { RenderContext renderContext = new(classInfo, classInfos, RenderOptions.CSharp); - SourceText source = SourceText.From(new CSharpInteropClassRenderer(classInfo, renderContext).Render(), Encoding.UTF8); + SourceText source = SourceText.From(new CSharpInteropClassRenderer(classInfo, renderContext, methodResolver).Render(), Encoding.UTF8); string outFileName = $"{classInfo.Name}.Interop.g.cs"; File.WriteAllText(Path.Combine(parsedArgs.CsOutputDir, outFileName), source.ToString()); } - JSObjectExtensionsRenderer jsObjectExtensionsRenderer = new(); - SourceText jsObjectExtensionsSource = SourceText.From(jsObjectExtensionsRenderer.Render(), Encoding.UTF8); + RenderContext jsObjRenderCtx = new(null, classInfos, RenderOptions.CSharp); + new JSObjectExtensionsRenderer(jsObjRenderCtx, resolvedTypes).Render(); + SourceText jsObjectExtensionsSource = SourceText.From(jsObjRenderCtx.ToString(), Encoding.UTF8); File.WriteAllText(Path.Combine(parsedArgs.CsOutputDir, "JSObjectExtensions.g.cs"), jsObjectExtensionsSource.ToString()); } diff --git a/TypeShim.Generator/SymbolMap.cs b/TypeShim.Generator/SymbolMap.cs index bc054ce6..50186215 100644 --- a/TypeShim.Generator/SymbolMap.cs +++ b/TypeShim.Generator/SymbolMap.cs @@ -1,4 +1,5 @@ using TypeShim.Generator.Parsing; +using TypeShim.Generator.Typescript; using TypeShim.Shared; namespace TypeShim.Generator; @@ -12,33 +13,4 @@ internal ClassInfo GetClassInfo(InteropTypeInfo type) _typeToClassDict.TryGetValue(type, out ClassInfo? info); return info ?? throw new NotFoundClassInfoException($"Could not find ClassInfo for type: {type.CSharpTypeSyntax}"); } - - internal string GetUserClassSymbolName(InteropTypeInfo type, TypeShimSymbolType flags) - { - ClassInfo classInfo = GetClassInfo(type.GetInnermostType()); - return GetUserClassSymbolNameCore(type, classInfo.Type, flags); - } - - internal string GetUserClassSymbolName(ClassInfo classInfo, TypeShimSymbolType flags) - { - return GetUserClassSymbolNameCore(classInfo.Type, classInfo.Type, flags); - } - - internal string GetUserClassSymbolName(ClassInfo classInfo, InteropTypeInfo useSiteTypeInfo, TypeShimSymbolType flags) - { - return GetUserClassSymbolNameCore(useSiteTypeInfo, classInfo.Type, flags); - } - - private static string GetUserClassSymbolNameCore(InteropTypeInfo useSiteTypeInfo, InteropTypeInfo userTypeInfo, TypeShimSymbolType flags) - { - return (flags) switch - { - TypeShimSymbolType.Proxy => useSiteTypeInfo.TypeScriptTypeSyntax.Render(), - TypeShimSymbolType.Namespace => useSiteTypeInfo.TypeScriptTypeSyntax.Render(), - TypeShimSymbolType.Snapshot => useSiteTypeInfo.TypeScriptTypeSyntax.Render(suffix: $".{RenderConstants.Properties}"), - TypeShimSymbolType.Initializer => useSiteTypeInfo.TypeScriptTypeSyntax.Render(suffix: $".{RenderConstants.Initializer}"), - TypeShimSymbolType.ProxyInitializerUnion => useSiteTypeInfo.TypeScriptTypeSyntax.Render(suffix: $" | {userTypeInfo.TypeScriptTypeSyntax.Render(suffix: $".{RenderConstants.Initializer}")}"), - _ => throw new NotImplementedException(), - }; - } } diff --git a/TypeShim.Generator/Typescript/TypeScriptMethodRenderer.cs b/TypeShim.Generator/Typescript/TypeScriptMethodRenderer.cs index 29d6098b..b3831324 100644 --- a/TypeShim.Generator/Typescript/TypeScriptMethodRenderer.cs +++ b/TypeShim.Generator/Typescript/TypeScriptMethodRenderer.cs @@ -1,4 +1,8 @@ -using System.Reflection; +using Microsoft.CodeAnalysis; +using System.Data.Common; +using System.Diagnostics; +using System.Reflection; +using System.Reflection.Metadata; using System.Text; using TypeShim.Generator.Parsing; using TypeShim.Shared; @@ -25,6 +29,7 @@ internal void RenderProxyConstructor(ConstructorInfo? constructorInfo) else { RenderConstructorSignature(); + ctx.Append(' '); RenderConstructorBody(); } @@ -35,20 +40,17 @@ void RenderConstructorSignature() if (constructorInfo.InitializerObject != null) { if (constructorInfo.Parameters.Length != 0) ctx.Append(", "); - - string returnType = ctx.SymbolMap.GetUserClassSymbolName(ctx.Class, TypeShimSymbolType.Initializer); - ctx.Append(constructorInfo.InitializerObject.Name).Append(": ").Append(returnType); + ctx.Append(constructorInfo.InitializerObject.Name).Append(": "); + TypeScriptSymbolNameRenderer.Render(ctx.Class.Type, ctx, TypeShimSymbolType.Initializer, interop: false); } ctx.Append(")"); } void RenderConstructorBody() { - ctx.AppendLine(" {"); + ctx.AppendLine("{"); using (ctx.Indent()) { - RenderHandleExtractionToConsts(constructorInfo.Parameters); - string proxyClassName = ctx.SymbolMap.GetUserClassSymbolName(ctx.Class, TypeShimSymbolType.Proxy); ctx.Append("super("); RenderInteropInvocation(constructorInfo.Name, constructorInfo.Parameters, constructorInfo.InitializerObject); ctx.AppendLine(");"); @@ -71,16 +73,7 @@ void RenderProxyMethodSignature(MethodInfo methodInfo) .Append('('); RenderParameterList(methodInfo.Parameters); ctx.Append("): "); - - if (methodInfo.ReturnType is { RequiresTypeConversion: true, SupportsTypeConversion: true }) - { - string returnTypeAsProxy = ctx.SymbolMap.GetUserClassSymbolName(methodInfo.ReturnType, TypeShimSymbolType.Proxy); - ctx.Append(returnTypeAsProxy); - } - else - { - ctx.Append(methodInfo.ReturnType.TypeScriptTypeSyntax.Render()); - } + RenderReturnType(methodInfo); } } @@ -102,16 +95,7 @@ void RenderProxyPropertyGetterSignature(MethodInfo methodInfo) ctx.Append($"public "); if (methodInfo.IsStatic) ctx.Append("static "); ctx.Append("get ").Append(propertyInfo.Name).Append("(): "); - - if (methodInfo.ReturnType is { RequiresTypeConversion: true, SupportsTypeConversion: true }) - { - string returnTypeAsProxy = ctx.SymbolMap.GetUserClassSymbolName(methodInfo.ReturnType, TypeShimSymbolType.Proxy); - ctx.Append(returnTypeAsProxy); - } - else - { - ctx.Append(methodInfo.ReturnType.TypeScriptTypeSyntax.Render()); - } + RenderReturnType(methodInfo); } void RenderProxyPropertySetterSignature(MethodInfo methodInfo) @@ -131,23 +115,26 @@ private void RenderParameterList(IEnumerable parameterInfos { if (!isFirst) ctx.Append(", "); - ctx.Append(parameterInfo.Name).Append(": ").Append(ResolveReturnType(parameterInfo.Type)); + ctx.Append(parameterInfo.Name).Append(": "); + + bool isDelegate = parameterInfo.Type.IsDelegateType() || (parameterInfo.Type.IsNullableType && parameterInfo.Type.TypeArgument!.IsDelegateType()); + TypeShimSymbolType returnSymbolType = parameterInfo.Type is { RequiresTypeConversion: true, SupportsTypeConversion: true } && !isDelegate + ? TypeShimSymbolType.ProxyInitializerUnion + : TypeShimSymbolType.None; + TypeScriptSymbolNameRenderer.Render(parameterInfo.Type, ctx, returnSymbolType, parameterSymbolType: TypeShimSymbolType.Proxy, interop: false); isFirst = false; } + } - string ResolveReturnType(InteropTypeInfo typeInfo) + private void RenderReturnType(MethodInfo methodInfo) + { + if (methodInfo.ReturnType is not { RequiresTypeConversion: true, SupportsTypeConversion: true }) { - if (!typeInfo.RequiresTypeConversion || !typeInfo.SupportsTypeConversion) - { - return typeInfo.TypeScriptTypeSyntax.Render(); - } - - ClassInfo classInfo = ctx.SymbolMap.GetClassInfo(typeInfo.GetInnermostType()); - TypeShimSymbolType symbolName = classInfo is { Constructor: { IsParameterless: true, AcceptsInitializer: true } } - ? TypeShimSymbolType.ProxyInitializerUnion // initializer also accepted - : TypeShimSymbolType.Proxy; - - return ctx.SymbolMap.GetUserClassSymbolName(classInfo, typeInfo, symbolName); + TypeScriptSymbolNameRenderer.Render(methodInfo.ReturnType, ctx); + } + else + { + TypeScriptSymbolNameRenderer.Render(methodInfo.ReturnType, ctx, returnSymbolType: TypeShimSymbolType.Proxy, parameterSymbolType: TypeShimSymbolType.ProxyInitializerUnion, interop: false); } } @@ -156,103 +143,212 @@ private void RenderMethodBody(MethodInfo methodInfo) ctx.AppendLine(" {"); using (ctx.Indent()) { - RenderHandleExtractionToConsts(methodInfo.Parameters); - - if (methodInfo.ReturnType is { RequiresTypeConversion: true, SupportsTypeConversion: true }) + bool requiresProxyConversion = methodInfo.ReturnType is { RequiresTypeConversion: true, SupportsTypeConversion: true }; + bool requiresCharConversion = RequiresCharConversion(methodInfo.ReturnType); + if (requiresProxyConversion || requiresCharConversion) { - string proxyClassName = ctx.SymbolMap.GetUserClassSymbolName(methodInfo.ReturnType.GetInnermostType(), TypeShimSymbolType.Proxy); - // user class return type, wrap in proxy ctx.Append("const res = "); RenderInteropInvocation(methodInfo.Name, methodInfo.Parameters); ctx.AppendLine(";"); + ctx.Append($"return "); - RenderInlineProxyConstruction(methodInfo.ReturnType, proxyClassName, "res"); - ctx.AppendLine(";"); + void resRenderer() => ctx.Append("res"); + RenderInlineProxyConstruction(methodInfo.ReturnType, resRenderer); } else { ctx.Append(methodInfo.ReturnType.ManagedType == KnownManagedType.Void ? string.Empty : "return "); - - RenderCharConversionIfNecessary(methodInfo.ReturnType, () => - { - RenderInteropInvocation(methodInfo.Name, methodInfo.Parameters); - }); - - ctx.AppendLine(";"); + RenderInteropInvocation(methodInfo.Name, methodInfo.Parameters); } + ctx.AppendLine(";"); } ctx.AppendLine("}"); + } - void RenderCharConversionIfNecessary(InteropTypeInfo typeInfo, Action renderCharExpression) + private void RenderInlineProxyConstruction(InteropTypeInfo typeInfo, Action expressionRenderer) + { + if (typeInfo.IsDelegateType()) { - // dotnet does not marshall chars as strings, instead as numbers. TypeShim converts to strings on the TS side. - if (methodInfo.ReturnType.ManagedType == KnownManagedType.Char) - ctx.Append("String.fromCharCode("); + RenderInlineDelegateHandleExtraction(typeInfo.ArgumentInfo!, expressionRenderer); + return; + } - renderCharExpression(); - - if (methodInfo.ReturnType.ManagedType == KnownManagedType.Char) - ctx.Append(")"); - if (methodInfo.ReturnType is { ManagedType: KnownManagedType.Task, TypeArgument.ManagedType: KnownManagedType.Char }) - ctx.Append(".then(c => String.fromCharCode(c))"); + if (typeInfo is { IsNullableType: true }) + { + expressionRenderer(); + ctx.Append(" ? "); + RenderInlineProxyConstruction(typeInfo.TypeArgument!, expressionRenderer); + ctx.Append(" : null"); + } + else if (typeInfo is { IsArrayType: true } or { IsTaskType: true }) + { + string transformFunction = typeInfo.IsArrayType ? "map" : "then"; + expressionRenderer(); + ctx.Append('.').Append(transformFunction).Append("(e => "); + RenderInlineProxyConstruction(typeInfo.TypeArgument!, () => ctx.Append("e")); + ctx.Append(')'); + } + else if (typeInfo.ManagedType == KnownManagedType.Char) + { + ctx.Append("String.fromCharCode("); + expressionRenderer(); + ctx.Append(")"); } + else + { + ctx.Append("ProxyBase.fromHandle("); + TypeScriptSymbolNameRenderer.Render(typeInfo, ctx, TypeShimSymbolType.Proxy, interop: false); + ctx.Append(", "); + expressionRenderer(); + ctx.Append(")"); + } + } - void RenderInlineProxyConstruction(InteropTypeInfo typeInfo, string proxyClassName, string sourceVarName) + private void RenderInlineDelegateProxyConstruction(DelegateArgumentInfo delegateInfo, Action targetDelegateExpressionRenderer) + { + ctx.Append("("); + foreach (var param in delegateInfo.ParameterTypes.Select((t, i) => new { Type = t, Index = i })) { - if (typeInfo is { IsNullableType: true }) - { - ctx.Append(sourceVarName).Append(" ? "); - RenderInlineProxyConstruction(typeInfo.TypeArgument!, proxyClassName, sourceVarName); - ctx.Append(" : null"); - } - else if (typeInfo is { IsArrayType: true } or { IsTaskType: true }) - { - string transformFunction = typeInfo.IsArrayType ? "map" : "then"; - ctx.Append(sourceVarName).Append('.').Append(transformFunction).Append("(e => "); - RenderInlineProxyConstruction(typeInfo.TypeArgument!, proxyClassName, "e"); - ctx.Append(')'); - } - else + if (param.Index > 0) ctx.Append(", "); + + ctx.Append("arg").Append(param.Index).Append(": "); + TypeScriptSymbolNameRenderer.Render(param.Type, ctx, TypeShimSymbolType.Proxy, interop: true); + } + ctx.Append(") => "); + + // Invoke target delegate (with optional return type conversion) + Action renderExpression = () => RenderTargetDelegateInvocation(delegateInfo, targetDelegateExpressionRenderer); + bool requiresRetVal = delegateInfo.ReturnType.IsNullableType || (delegateInfo.ReturnType.RequiresTypeConversion && delegateInfo.ReturnType.SupportsTypeConversion); + if (requiresRetVal) + { + ctx.Append("{ const retVal = "); + renderExpression(); + ctx.Append("; return "); + renderExpression = () => ctx.Append("retVal"); + } + + RenderInlineHandleExtraction(delegateInfo.ReturnType, renderExpression); + if (requiresRetVal) + { + ctx.Append(" }"); + } + + void RenderTargetDelegateInvocation(DelegateArgumentInfo delegateInfo, Action targetDelegateExpressionRenderer) + { + targetDelegateExpressionRenderer(); + ctx.Append("("); + foreach (var param in delegateInfo.ParameterTypes.Select((t, i) => new { Type = t, Index = i })) { - ctx.Append($"ProxyBase.fromHandle({proxyClassName}, {sourceVarName})"); + if (param.Index > 0) ctx.Append(", "); + + if (param.Type.RequiresTypeConversion && param.Type.SupportsTypeConversion || RequiresCharConversion(param.Type)) + { + RenderInlineProxyConstruction(param.Type, () => ctx.Append("arg" + param.Index)); + } + else + { + ctx.Append("arg").Append(param.Index); + } } + ctx.Append(")"); } } - private void RenderHandleExtractionToConsts(IEnumerable parameterInfos) + private void RenderInlineHandleExtraction(InteropTypeInfo typeInfo, Action expressionRenderer) { - foreach (MethodParameterInfo parameterInfo in parameterInfos) + if (typeInfo.IsDelegateType()) { - if (parameterInfo.IsInjectedInstanceParameter || !parameterInfo.Type.RequiresTypeConversion || !parameterInfo.Type.SupportsTypeConversion) - continue; + RenderInlineDelegateProxyConstruction(typeInfo.ArgumentInfo!, expressionRenderer); + return; + } - ctx.Append("const "); - ctx.Append(GetInteropInvocationVariable(parameterInfo)); - ctx.Append(" = "); - string proxyClassName = ctx.SymbolMap.GetUserClassSymbolName(parameterInfo.Type.GetInnermostType(), TypeShimSymbolType.Proxy); - RenderInlineHandleExtraction(parameterInfo.Type, proxyClassName, parameterInfo.Name); - ctx.AppendLine(";"); + expressionRenderer(); + if (typeInfo is { IsNullableType: true }) + { + ctx.Append(" ? "); + RenderInlineHandleExtraction(typeInfo.TypeArgument!, expressionRenderer); + ctx.Append(" : null"); } + else if (typeInfo.ManagedType == KnownManagedType.Char) + { + ctx.Append(".charCodeAt(0)"); + } + else if (typeInfo is { IsArrayType: true } or { IsTaskType: true } && typeInfo.TypeArgument != null) + { + string transformFunction = typeInfo.IsArrayType ? "map" : "then"; + ctx.Append('.').Append(transformFunction).Append("(e => "); + RenderInlineHandleExtraction(typeInfo.TypeArgument, () => ctx.Append("e")); + ctx.Append(')'); + } + else if (typeInfo.IsTSExport && ctx.SymbolMap.GetClassInfo(typeInfo) is { Constructor: { AcceptsInitializer: true, IsParameterless: true } }) + { + // accepts initializer or proxy, if proxy, extract handle, if init, pass as is + ctx.Append(" instanceof "); + TypeScriptSymbolNameRenderer.Render(typeInfo, ctx, TypeShimSymbolType.Proxy, interop: false); + ctx.Append(" ? "); + expressionRenderer(); + ctx.Append(".instance : "); + expressionRenderer(); + } + else if (typeInfo.IsTSExport) // simple proxy + { + ctx.Append(".instance"); + } + } - void RenderInlineHandleExtraction(InteropTypeInfo typeInfo, string proxyClassName, string sourceVarName) + /// + /// Renders the delegate that wraps the user delegate to extract handles from proxies before invoking the target delegate. + /// Used when passing a delegate from TS to .NET. + /// + /// + /// + private void RenderInlineDelegateHandleExtraction(DelegateArgumentInfo delegateInfo, Action expressionRenderer) + { + // Build signature + ctx.Append("("); + foreach (var param in delegateInfo.ParameterTypes.Select((t, i) => new { Type = t, Index = i })) { - if (typeInfo is { IsNullableType: true }) - { - ctx.Append(sourceVarName).Append(" ? "); - RenderInlineHandleExtraction(typeInfo.TypeArgument!, proxyClassName, sourceVarName); - ctx.Append(" : null"); - } - else if (typeInfo is { IsArrayType: true } or { IsTaskType: true }) - { - string transformFunction = typeInfo.IsArrayType ? "map" : "then"; - ctx.Append(sourceVarName).Append('.').Append(transformFunction).Append("(e => "); - RenderInlineHandleExtraction(typeInfo.TypeArgument!, proxyClassName, "e"); - ctx.Append(')'); - } - else + if (param.Index > 0) ctx.Append(", "); + ctx.Append("arg").Append(param.Index).Append(": "); + TypeScriptSymbolNameRenderer.Render(param.Type, ctx, TypeShimSymbolType.ProxyInitializerUnion, interop: false); + } + ctx.Append(") => "); + + Action renderExpression = () => RenderTargetDelegateInvocation(delegateInfo, expressionRenderer); + bool requiresRetVal = delegateInfo.ReturnType.IsNullableType || (delegateInfo.ReturnType.RequiresTypeConversion && delegateInfo.ReturnType.SupportsTypeConversion); + if (requiresRetVal) + { + ctx.Append("{ const retVal = "); + renderExpression(); + ctx.Append("; return "); + renderExpression = () => ctx.Append("retVal"); + } + + if (delegateInfo.ReturnType.RequiresTypeConversion && delegateInfo.ReturnType.SupportsTypeConversion || RequiresCharConversion(delegateInfo.ReturnType)) + { + RenderInlineProxyConstruction(delegateInfo.ReturnType, renderExpression); + } + else + { + renderExpression(); + } + + if (requiresRetVal) + { + ctx.Append(" }"); + } + + void RenderTargetDelegateInvocation(DelegateArgumentInfo delegateInfo, Action expressionRenderer) + { + expressionRenderer(); + ctx.Append("("); + foreach (var param in delegateInfo.ParameterTypes.Select((t, i) => new { Type = t, Index = i })) { - ctx.Append(sourceVarName).Append(" instanceof ").Append(proxyClassName).Append(" ? ").Append(sourceVarName).Append(".instance : ").Append(sourceVarName); + if (param.Index > 0) ctx.Append(", "); + + RenderInlineHandleExtraction(param.Type, () => ctx.Append("arg").Append(param.Index)); } + ctx.Append(")"); } } @@ -271,14 +367,25 @@ void RenderMethodInvocationParameters(string instanceParameterExpression) { if (!isFirst) ctx.Append(", "); - ctx.Append(parameter.IsInjectedInstanceParameter ? instanceParameterExpression : GetInteropInvocationVariable(parameter)); - RenderCharConversionIfNecessary(parameter); + void renderParameter() => ctx.Append(parameter.Name); + if (parameter.IsInjectedInstanceParameter) + { + ctx.Append(instanceParameterExpression); + } + else if (parameter.Type.RequiresTypeConversion && parameter.Type.SupportsTypeConversion || RequiresCharConversion(parameter.Type)) + { + RenderInlineHandleExtraction(parameter.Type, renderParameter); + } + else + { + renderParameter(); + } isFirst = false; } if (initializerObject == null) return; if (!isFirst) ctx.Append(", "); - ctx.Append(initializerObject.Name); + RenderInitializerParameter(initializerObject); } void RenderInteropMethodAccessor(string methodName) @@ -286,18 +393,36 @@ void RenderInteropMethodAccessor(string methodName) ctx.Append(ctx.Class.Namespace).Append('.').Append(RenderConstants.InteropClassName(ctx.Class)).Append('.').Append(methodName); } - void RenderCharConversionIfNecessary(MethodParameterInfo parameter) + void RenderInitializerParameter(MethodParameterInfo initializerObject) { - // dotnet does not marshall chars as strings atm. We convert from/to numbers while this is the case. - if (parameter.Type.ManagedType == KnownManagedType.Char) - ctx.Append(".charCodeAt(0)"); - if (parameter.Type is { ManagedType: KnownManagedType.Task, TypeArgument.ManagedType: KnownManagedType.Char }) - ctx.Append(".then(c => c.charCodeAt(0))"); + ctx.Append("{ ...").Append(initializerObject.Name); + foreach (PropertyInfo propertyInfo in ctx.Class.Properties) + { + bool requiresProxyConversion = propertyInfo.Type.RequiresTypeConversion && propertyInfo.Type.SupportsTypeConversion; + bool requiresCharConversion = RequiresCharConversion(propertyInfo.Type); + if (!requiresCharConversion && !requiresProxyConversion) + { + continue; + } + + void renderPropertyAccessorExpression() => ctx.Append(initializerObject.Name).Append('.').Append(propertyInfo.Name); + ctx.Append(", ").Append(propertyInfo.Name).Append(": "); + RenderInlineHandleExtraction(propertyInfo.Type, renderPropertyAccessorExpression); + } + ctx.Append(" }"); } } - private static string GetInteropInvocationVariable(MethodParameterInfo param) // TODO: get from ctx localscope (check param.Name call sites!) + private static bool RequiresCharConversion(InteropTypeInfo typeInfo) { - return param.Type.RequiresTypeConversion && param.Type.SupportsTypeConversion ? $"{param.Name}Instance" : param.Name; + // dotnet does not marshall chars as strings atm. TypeShim converts from/to numbers while crossing the boundary. + return typeInfo switch + { + { ManagedType: KnownManagedType.Nullable } => RequiresCharConversion(typeInfo.TypeArgument!), + { ManagedType: KnownManagedType.Task } => RequiresCharConversion(typeInfo.TypeArgument!), + { ManagedType: KnownManagedType.Char } => true, + { ArgumentInfo: DelegateArgumentInfo argumentInfo } when (typeInfo.IsDelegateType()) => RequiresCharConversion(argumentInfo.ReturnType) || argumentInfo.ParameterTypes.Any(RequiresCharConversion), + _ => false + }; } } \ No newline at end of file diff --git a/TypeShim.Generator/Typescript/TypeScriptSymbolNameRenderer.cs b/TypeShim.Generator/Typescript/TypeScriptSymbolNameRenderer.cs new file mode 100644 index 00000000..7770be40 --- /dev/null +++ b/TypeShim.Generator/Typescript/TypeScriptSymbolNameRenderer.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Text; +using TypeShim.Generator.Parsing; +using TypeShim.Shared; + +namespace TypeShim.Generator.Typescript; + +internal class TypeScriptSymbolNameRenderer(TypeShimSymbolType returnSymbolType, TypeShimSymbolType parameterSymbolType, bool interop, RenderContext ctx) +{ + public static void Render(InteropTypeInfo typeInfo, RenderContext ctx) + { + TypeScriptSymbolNameRenderer renderer = new(TypeShimSymbolType.None, TypeShimSymbolType.None, interop: false, ctx); + renderer.RenderCore(typeInfo); + } + + public static void Render(InteropTypeInfo typeInfo, RenderContext ctx, TypeShimSymbolType symbolType, bool interop) + { + TypeScriptSymbolNameRenderer renderer = new(symbolType, symbolType, interop, ctx); + renderer.RenderCore(typeInfo); + } + + public static void Render(InteropTypeInfo typeInfo, RenderContext ctx, TypeShimSymbolType returnSymbolType, TypeShimSymbolType parameterSymbolType, bool interop) + { + TypeScriptSymbolNameRenderer renderer = new(returnSymbolType, parameterSymbolType, interop, ctx); + renderer.RenderCore(typeInfo); + } + + private void RenderCore(InteropTypeInfo typeInfo, bool isDelegateParameter = false) + { + if (typeInfo.IsDelegateType()) + { + RenderDelegateCore(typeInfo.ArgumentInfo!); + } + else if (typeInfo.IsNullableType) + { + RenderNullableCore(typeInfo); + } + else if (typeInfo.IsArrayType) + { + RenderArrayCore(typeInfo); + } + else if (typeInfo.IsTaskType) + { + RenderPromiseCore(typeInfo); + } + else + { + ctx.Append(GetSymbolNameTemplate(typeInfo).Template); + if (typeInfo.IsTSExport) + { + RenderSuffix(typeInfo, isDelegateParameter ? parameterSymbolType : returnSymbolType); + } + } + } + + private void RenderNullableCore(InteropTypeInfo typeInfo) + { + bool isNullableDelegate = typeInfo.TypeArgument?.IsDelegateType() == true; + if (isNullableDelegate) ctx.Append("("); + + RenderCore(typeInfo.TypeArgument!); + + if (isNullableDelegate) ctx.Append(")"); + + ctx.Append(" | null"); + } + + private void RenderArrayCore(InteropTypeInfo typeInfo) + { + ctx.Append("Array<"); + RenderCore(typeInfo.TypeArgument!); + ctx.Append(">"); + } + + private void RenderPromiseCore(InteropTypeInfo typeInfo) + { + if (typeInfo.TypeArgument == null) + { + ctx.Append("Promise"); + } + else + { + ctx.Append("Promise<"); + RenderCore(typeInfo.TypeArgument!); + ctx.Append(">"); + } + } + + private void RenderDelegateCore(DelegateArgumentInfo delegateInfo) + { + ctx.Append("("); + foreach (var param in delegateInfo.ParameterTypes.Select((t, i) => new { Type = t, Index = i })) + { + if (param.Index > 0) ctx.Append(", "); + ctx.Append("arg").Append(param.Index).Append(": "); + RenderCore(param.Type, isDelegateParameter: true); + } + ctx.Append(") => "); + RenderCore(delegateInfo.ReturnType); + } + + private TypeScriptSymbolNameTemplate GetSymbolNameTemplate(InteropTypeInfo typeInfo) + => interop ? typeInfo.TypeScriptInteropTypeSyntax : typeInfo.TypeScriptTypeSyntax; + + private void RenderSuffix(InteropTypeInfo typeInfo, TypeShimSymbolType symbolType) + { + if (typeInfo.GetInnermostType() is not { IsTSExport: true } innerMostTSExport + || symbolType is TypeShimSymbolType.Proxy or TypeShimSymbolType.Namespace or TypeShimSymbolType.None) + { + return; + } + + if (symbolType is TypeShimSymbolType.Snapshot) + { + ctx.Append('.').Append(RenderConstants.Properties); + return; + } + + if (symbolType is TypeShimSymbolType.Initializer) + { + ctx.Append('.').Append(RenderConstants.Initializer); + return; + } + + if (symbolType is TypeShimSymbolType.ProxyInitializerUnion) + { + RenderProxyInitializerSuffix(interop, innerMostTSExport); + return; + } + + throw new NotImplementedException($"Unhandled type/symboltype combination for {typeInfo.CSharpTypeSyntax} {symbolType}"); + + void RenderProxyInitializerSuffix(bool interop, InteropTypeInfo innerMostTSExport) + { + if (ctx.SymbolMap.GetClassInfo(innerMostTSExport) is not { Constructor: { AcceptsInitializer: true, IsParameterless: true } }) + { + RenderSuffix(typeInfo, TypeShimSymbolType.Proxy); // initializer not supported, fall back to proxy only. + } + else if (interop) + { + ctx.Append(" | object"); + } + else + { + ctx.Append(" | "); + TypeScriptSymbolNameRenderer innerRenderer = new(TypeShimSymbolType.Initializer, TypeShimSymbolType.Initializer, interop, ctx); + innerRenderer.RenderCore(innerMostTSExport); + } + } + } + +} diff --git a/TypeShim.Generator/Typescript/TypeScriptUserClassNamespaceRenderer.cs b/TypeShim.Generator/Typescript/TypeScriptUserClassNamespaceRenderer.cs index 2eb95342..28505890 100644 --- a/TypeShim.Generator/Typescript/TypeScriptUserClassNamespaceRenderer.cs +++ b/TypeShim.Generator/Typescript/TypeScriptUserClassNamespaceRenderer.cs @@ -9,21 +9,26 @@ internal void Render() { if (ctx.Class.IsStatic) return; - PropertyInfo[] instancePropertyInfos = [.. ctx.Class.Properties.Where(p => !p.IsStatic)]; - if (instancePropertyInfos.Length == 0) + PropertyInfo[] instancePropertyInfos = [.. ctx.Class.Properties.Where(p => !p.IsStatic && !p.Type.IsDelegateType())]; + PropertyInfo[] initializerPropertyInfos = ctx.Class.Constructor?.MemberInitializers ?? []; + if (initializerPropertyInfos.Length == 0 && instancePropertyInfos.Length == 0) return; ctx.AppendLine($"export namespace {ctx.Class.Name} {{"); using (ctx.Indent()) { TypeScriptUserClassShapesRenderer shapesRenderer = new(ctx); - if (ctx.Class.Constructor?.MemberInitializers is { Length: > 0 } initializerPropertyInfos) + if (initializerPropertyInfos.Length > 0) { shapesRenderer.RenderInitializerInterface(initializerPropertyInfos); } - shapesRenderer.RenderPropertiesInterface(instancePropertyInfos); - const string proxyParamName = "proxy"; - shapesRenderer.RenderPropertiesFunction(proxyParamName); + + if (instancePropertyInfos.Length > 0) + { + shapesRenderer.RenderPropertiesInterface(instancePropertyInfos); + const string proxyParamName = "proxy"; + shapesRenderer.RenderPropertiesFunction(proxyParamName); + } } ctx.AppendLine("}"); } diff --git a/TypeShim.Generator/Typescript/TypeScriptUserClassShapesRenderer.cs b/TypeShim.Generator/Typescript/TypeScriptUserClassShapesRenderer.cs index da7f9867..78a34f48 100644 --- a/TypeShim.Generator/Typescript/TypeScriptUserClassShapesRenderer.cs +++ b/TypeShim.Generator/Typescript/TypeScriptUserClassShapesRenderer.cs @@ -19,12 +19,11 @@ internal void RenderPropertiesInterface(PropertyInfo[] propertyInfos) ctx.Append(propertyInfo.Name).Append(": "); if (propertyInfo.Type is { RequiresTypeConversion: true, SupportsTypeConversion: true }) { - string snapshotSymbolName = ctx.SymbolMap.GetUserClassSymbolName(propertyInfo.Type, TypeShimSymbolType.Snapshot); - ctx.Append(snapshotSymbolName); + TypeScriptSymbolNameRenderer.Render(propertyInfo.Type, ctx, TypeShimSymbolType.Snapshot, interop: false); } else { - ctx.Append(propertyInfo.Type.TypeScriptTypeSyntax.Render()); + TypeScriptSymbolNameRenderer.Render(propertyInfo.Type, ctx); } ctx.AppendLine(";"); } @@ -42,15 +41,12 @@ internal void RenderInitializerInterface(PropertyInfo[] propertyInfos) ctx.Append(propertyInfo.Name).Append(": "); if (propertyInfo.Type is { RequiresTypeConversion: true, SupportsTypeConversion: true }) { - ClassInfo classInfo = ctx.SymbolMap.GetClassInfo(propertyInfo.Type.GetInnermostType()); - TypeShimSymbolType symbolType = classInfo is { Constructor: { IsParameterless: true, AcceptsInitializer: true } } - ? TypeShimSymbolType.ProxyInitializerUnion - : TypeShimSymbolType.Proxy; - ctx.Append(ctx.SymbolMap.GetUserClassSymbolName(classInfo, propertyInfo.Type, symbolType)); + TypeShimSymbolType returnSymbolType = propertyInfo.Type.IsDelegateType() ? TypeShimSymbolType.Proxy : TypeShimSymbolType.ProxyInitializerUnion; + TypeScriptSymbolNameRenderer.Render(propertyInfo.Type, ctx, returnSymbolType, parameterSymbolType: TypeShimSymbolType.ProxyInitializerUnion, interop: false); } else { - ctx.Append(propertyInfo.Type.TypeScriptTypeSyntax.Render()); + TypeScriptSymbolNameRenderer.Render(propertyInfo.Type, ctx); } ctx.AppendLine(";"); } @@ -60,9 +56,11 @@ internal void RenderInitializerInterface(PropertyInfo[] propertyInfos) internal void RenderPropertiesFunction(string proxyParamName) { - string paramType = ctx.SymbolMap.GetUserClassSymbolName(ctx.Class, TypeShimSymbolType.Proxy); - string returnType = ctx.SymbolMap.GetUserClassSymbolName(ctx.Class, TypeShimSymbolType.Snapshot); - ctx.Append($"export function ").Append(RenderConstants.ProxyMaterializeFunction).Append('(').Append(proxyParamName).Append(": ").Append(paramType).Append("): ").Append(returnType).AppendLine(" {"); + ctx.Append($"export function ").Append(RenderConstants.ProxyMaterializeFunction).Append('(').Append(proxyParamName).Append(": "); + TypeScriptSymbolNameRenderer.Render(ctx.Class.Type, ctx, TypeShimSymbolType.Proxy, interop: false); + ctx.Append("): "); + TypeScriptSymbolNameRenderer.Render(ctx.Class.Type, ctx, TypeShimSymbolType.Snapshot, interop: false); + ctx.AppendLine(" {"); using (ctx.Indent()) { RenderFunctionBody(proxyParamName); @@ -74,23 +72,25 @@ void RenderFunctionBody(string proxyParamName) ctx.AppendLine("return {"); using (ctx.Indent()) { - foreach (PropertyInfo propertyInfo in ctx.Class.Properties.Where(p => !p.IsStatic)) + foreach (PropertyInfo propertyInfo in ctx.Class.Properties.Where(p => !p.IsStatic && !p.Type.IsDelegateType())) { - ctx.Append(propertyInfo.Name).Append(": ") - .Append(GetPropertyValueExpression(propertyInfo.Type, $"{proxyParamName}.{propertyInfo.Name}")) - .AppendLine(","); + ctx.Append(propertyInfo.Name).Append(": "); + RenderPropertyValueExpression(propertyInfo.Type, $"{proxyParamName}.{propertyInfo.Name}"); + ctx.AppendLine(","); } } ctx.AppendLine("};"); } // TODO: refactor to RenderPropertyValueExpression (use ctx) - string GetPropertyValueExpression(InteropTypeInfo typeInfo, string propertyAccessorExpression) + void RenderPropertyValueExpression(InteropTypeInfo typeInfo, string propertyAccessorExpression) { if (typeInfo.IsNullableType) { InteropTypeInfo innerTypeInfo = typeInfo.TypeArgument ?? throw new InvalidOperationException("Nullable type must have a type argument."); - return $"{propertyAccessorExpression} ? {GetPropertyValueExpression(innerTypeInfo, propertyAccessorExpression)} : null"; + ctx.Append(propertyAccessorExpression).Append(" ? "); + RenderPropertyValueExpression(innerTypeInfo, propertyAccessorExpression); + ctx.Append(" : null"); } else if (typeInfo.RequiresTypeConversion && typeInfo.SupportsTypeConversion) { @@ -98,17 +98,19 @@ string GetPropertyValueExpression(InteropTypeInfo typeInfo, string propertyAcces { InteropTypeInfo elementTypeInfo = typeInfo.TypeArgument ?? throw new InvalidOperationException("Conversion-requiring array/task type must have a type argument."); string transformFunction = typeInfo.IsArrayType ? "map" : "then"; - return $"{propertyAccessorExpression}.{transformFunction}(e => {GetPropertyValueExpression(elementTypeInfo, "e")})"; + ctx.Append(propertyAccessorExpression).Append('.').Append(transformFunction).Append("(e => "); + RenderPropertyValueExpression(elementTypeInfo, "e"); + ctx.Append(')'); } else // exported user type { - string tsNamespace = ctx.SymbolMap.GetUserClassSymbolName(typeInfo, TypeShimSymbolType.Namespace); - return $"{tsNamespace}.{RenderConstants.ProxyMaterializeFunction}({propertyAccessorExpression})"; + TypeScriptSymbolNameRenderer.Render(typeInfo, ctx, TypeShimSymbolType.Namespace, interop: false); + ctx.Append('.').Append(RenderConstants.ProxyMaterializeFunction).Append('(').Append(propertyAccessorExpression).Append(')'); } } else // simple primitive or unconvertable class { - return propertyAccessorExpression; + ctx.Append(propertyAccessorExpression); } } } diff --git a/TypeShim.Generator/Typescript/TypescriptAssemblyExportsRenderer.cs b/TypeShim.Generator/Typescript/TypescriptAssemblyExportsRenderer.cs index 45acc5f7..c0b39aec 100644 --- a/TypeShim.Generator/Typescript/TypescriptAssemblyExportsRenderer.cs +++ b/TypeShim.Generator/Typescript/TypescriptAssemblyExportsRenderer.cs @@ -64,21 +64,15 @@ private void RenderInteropMethodSignature(string name, IEnumerable GetAllMethods(ClassInfo classInfo) diff --git a/TypeShim.Shared/InteropTypeInfo.cs b/TypeShim.Shared/InteropTypeInfo.cs index d352a20c..e4125c8d 100644 --- a/TypeShim.Shared/InteropTypeInfo.cs +++ b/TypeShim.Shared/InteropTypeInfo.cs @@ -1,10 +1,16 @@ using System; +using System.Linq; using System.Collections.Generic; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace TypeShim.Shared; +internal sealed class DelegateArgumentInfo +{ + public required InteropTypeInfo ReturnType { get; init; } + public required InteropTypeInfo[] ParameterTypes { get; init; } +} internal sealed class InteropTypeInfo { public required KnownManagedType ManagedType { get; init; } @@ -29,13 +35,20 @@ internal sealed class InteropTypeInfo public required TypeScriptSymbolNameTemplate TypeScriptInteropTypeSyntax { get; init; } /// - /// Tasks and Arrays _may_ have type arguments + /// Tasks and Arrays _may_ have type arguments. Nullables always do. /// public required InteropTypeInfo? TypeArgument { get; init; } - public required bool IsTaskType { get; init; } - public required bool IsArrayType { get; init; } - public required bool IsNullableType { get; init; } + /// + /// For delegates + /// + public required DelegateArgumentInfo? ArgumentInfo { get; init; } + + public required bool IsTaskType { get; init; } // TODO: swap out for KnownManagedType check? + public required bool IsArrayType { get; init; } // TODO: swap out for KnownManagedType check? + public required bool IsNullableType { get; init; } // TODO: swap out for KnownManagedType check? + + public bool IsDelegateType() => ManagedType is KnownManagedType.Function or KnownManagedType.Action; public required bool IsTSExport { get; init; } public required bool RequiresTypeConversion { get; init; } @@ -72,6 +85,11 @@ public InteropTypeInfo AsInteropTypeInfo() IsArrayType = this.IsArrayType, IsNullableType = this.IsNullableType, TypeArgument = TypeArgument?.AsInteropTypeInfo(), + ArgumentInfo = this.ArgumentInfo is not DelegateArgumentInfo argInfo ? null : new DelegateArgumentInfo() + { + ParameterTypes = [.. argInfo.ParameterTypes.Select(argType => argType.AsInteropTypeInfo())], + ReturnType = argInfo.ReturnType.AsInteropTypeInfo() + }, RequiresTypeConversion = false, SupportsTypeConversion = false, }; @@ -91,6 +109,7 @@ public InteropTypeInfo AsInteropTypeInfo() IsNullableType = false, RequiresTypeConversion = false, TypeArgument = null, + ArgumentInfo = null, SupportsTypeConversion = false, }; } diff --git a/TypeShim.Shared/InteropTypeInfoBuilder.cs b/TypeShim.Shared/InteropTypeInfoBuilder.cs index 63e7431a..c2b61452 100644 --- a/TypeShim.Shared/InteropTypeInfoBuilder.cs +++ b/TypeShim.Shared/InteropTypeInfoBuilder.cs @@ -30,11 +30,74 @@ private InteropTypeInfo BuildInternal() JSNullableTypeInfo nullableTypeInfo => BuildNullableTypeInfo(nullableTypeInfo, clrTypeSyntax), JSSpanTypeInfo => throw new NotImplementedException("Span is not yet supported"), JSArraySegmentTypeInfo => throw new NotImplementedException("ArraySegment is not yet supported"), - JSFunctionTypeInfo => throw new NotImplementedException("Func & Action are not yet supported"), + JSFunctionTypeInfo functionTypeInfo => BuildFunctionTypeInfo(functionTypeInfo, clrTypeSyntax), JSInvalidTypeInfo or _ => throw new NotSupportedTypeException(typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)), }; } + private InteropTypeInfo BuildFunctionTypeInfo(JSFunctionTypeInfo functionTypeInfo, TypeSyntax clrTypeSyntax) + { + (InteropTypeInfo[] argTypeInfos, InteropTypeInfo returnTypeInfo) = GetArgumentTypeInfos(typeSymbol); + InteropTypeInfo[] allArgTypeInfos = [.. argTypeInfos, returnTypeInfo]; + + DelegateArgumentInfo argumentInfo = new() + { + ParameterTypes = argTypeInfos, + ReturnType = returnTypeInfo + }; + + TypeScriptFunctionParameterTemplate[] tsParameterTemplates = [.. argTypeInfos.Select((InteropTypeInfo typeInfo, int i) => + new TypeScriptFunctionParameterTemplate($"arg{i}", GetSimpleTypeScriptSymbolTemplate(typeInfo.ManagedType, typeInfo.CSharpTypeSyntax, typeInfo.RequiresTypeConversion, typeInfo.SupportsTypeConversion)) + )]; + TypeScriptSymbolNameTemplate tsSyntax = TypeScriptSymbolNameTemplate.ForDelegateType(argumentInfo); + + return new InteropTypeInfo + { + ManagedType = functionTypeInfo.KnownType, + JSTypeSyntax = GetJSTypeSyntax(functionTypeInfo, clrTypeSyntax), + CSharpInteropTypeSyntax = GetCSInteropTypeSyntax(functionTypeInfo), + CSharpTypeSyntax = clrTypeSyntax, + TypeScriptTypeSyntax = tsSyntax, + TypeScriptInteropTypeSyntax = tsSyntax, + TypeArgument = null, + ArgumentInfo = argumentInfo, + IsTaskType = false, + IsArrayType = false, + IsNullableType = false, + IsTSExport = IsTSExport, + RequiresTypeConversion = allArgTypeInfos.Any(info => info.RequiresTypeConversion), + SupportsTypeConversion = allArgTypeInfos.Any(info => info.RequiresTypeConversion && info.SupportsTypeConversion) + }; + } + + private (InteropTypeInfo[] Parameters, InteropTypeInfo ReturnType) GetArgumentTypeInfos(ITypeSymbol typeSymbol) + { + string fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + PredefinedTypeSyntax voidSyntax = SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)); + JSSimpleTypeInfo voidJSTypeInfo = new JSSimpleTypeInfo(KnownManagedType.Void) + { + Syntax = voidSyntax + }; + switch (typeSymbol) + { + case ITypeSymbol when fullTypeName == Constants.ActionGlobal: + return (Parameters: [], ReturnType: BuildSimpleTypeInfo(voidJSTypeInfo, voidSyntax)); + + case INamedTypeSymbol actionType when fullTypeName.StartsWith(Constants.ActionGlobal, StringComparison.Ordinal): + InteropTypeInfo[] argumentTypes = [.. actionType.TypeArguments.Select(arg => new InteropTypeInfoBuilder(arg, cache).Build())]; + return (Parameters: argumentTypes, ReturnType: BuildSimpleTypeInfo(voidJSTypeInfo, voidSyntax)); + + // function + case INamedTypeSymbol funcType when fullTypeName.StartsWith(Constants.FuncGlobal, StringComparison.Ordinal): + InteropTypeInfo[] signatureTypes = [.. funcType.TypeArguments.Select(arg => new InteropTypeInfoBuilder(arg, cache).Build())]; + return (Parameters: [.. signatureTypes.Take(signatureTypes.Length - 1)], ReturnType: signatureTypes.Last()); + } + throw new NotSupportedTypeException($"Delegate type '{typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)}' has an unsupported argument types"); + } + + + + private InteropTypeInfo BuildSimpleTypeInfo(JSSimpleTypeInfo simpleTypeInfo, TypeSyntax clrTypeSyntax) { bool requiresTypeConversion = RequiresTypeConversion(); @@ -44,11 +107,12 @@ private InteropTypeInfo BuildSimpleTypeInfo(JSSimpleTypeInfo simpleTypeInfo, Typ { ManagedType = simpleTypeInfo.KnownType, JSTypeSyntax = GetJSTypeSyntax(simpleTypeInfo, clrTypeSyntax), - CSharpInteropTypeSyntax = GetInteropTypeSyntax(simpleTypeInfo), + CSharpInteropTypeSyntax = GetCSInteropTypeSyntax(simpleTypeInfo), CSharpTypeSyntax = clrTypeSyntax, TypeScriptTypeSyntax = GetSimpleTypeScriptSymbolTemplate(simpleTypeInfo.KnownType, clrTypeSyntax, requiresTypeConversion, supportsTypeConversion), TypeScriptInteropTypeSyntax = GetInteropSimpleTypeScriptSymbolTemplate(simpleTypeInfo.KnownType, clrTypeSyntax), TypeArgument = null, + ArgumentInfo = null, IsTaskType = false, IsArrayType = false, IsNullableType = clrTypeSyntax is NullableTypeSyntax, @@ -84,11 +148,12 @@ private InteropTypeInfo BuildArrayTypeInfo(JSArrayTypeInfo arrayTypeInfo, TypeSy { ManagedType = arrayTypeInfo.KnownType, JSTypeSyntax = GetJSTypeSyntax(arrayTypeInfo, clrTypeSyntax), - CSharpInteropTypeSyntax = GetInteropTypeSyntax(arrayTypeInfo), + CSharpInteropTypeSyntax = GetCSInteropTypeSyntax(arrayTypeInfo), CSharpTypeSyntax = clrTypeSyntax, - TypeScriptTypeSyntax = TypeScriptSymbolNameTemplate.ForArrayType(elementTypeInfo.TypeScriptTypeSyntax), - TypeScriptInteropTypeSyntax = TypeScriptSymbolNameTemplate.ForArrayType(elementTypeInfo.TypeScriptInteropTypeSyntax), + TypeScriptTypeSyntax = TypeScriptSymbolNameTemplate.ForArrayType(elementTypeInfo), + TypeScriptInteropTypeSyntax = TypeScriptSymbolNameTemplate.ForArrayType(elementTypeInfo), TypeArgument = elementTypeInfo, + ArgumentInfo = null, IsTaskType = false, IsArrayType = true, IsNullableType = false, @@ -116,11 +181,12 @@ private InteropTypeInfo BuildTaskTypeInfo(JSTaskTypeInfo taskTypeInfo, TypeSynta { ManagedType = taskTypeInfo.KnownType, JSTypeSyntax = GetJSTypeSyntax(taskTypeInfo, clrTypeSyntax), - CSharpInteropTypeSyntax = GetInteropTypeSyntax(taskTypeInfo), + CSharpInteropTypeSyntax = GetCSInteropTypeSyntax(taskTypeInfo), CSharpTypeSyntax = clrTypeSyntax, - TypeScriptTypeSyntax = TypeScriptSymbolNameTemplate.ForPromiseType(taskReturnTypeInfo?.TypeScriptTypeSyntax), - TypeScriptInteropTypeSyntax = TypeScriptSymbolNameTemplate.ForPromiseType(taskReturnTypeInfo?.TypeScriptInteropTypeSyntax), + TypeScriptTypeSyntax = TypeScriptSymbolNameTemplate.ForPromiseType(taskReturnTypeInfo), + TypeScriptInteropTypeSyntax = TypeScriptSymbolNameTemplate.ForPromiseType(taskReturnTypeInfo), TypeArgument = taskReturnTypeInfo, + ArgumentInfo = null, IsTaskType = true, IsArrayType = false, IsNullableType = false, @@ -151,11 +217,12 @@ private InteropTypeInfo BuildNullableTypeInfo(JSNullableTypeInfo nullableTypeInf { ManagedType = nullableTypeInfo.KnownType, JSTypeSyntax = GetJSTypeSyntax(nullableTypeInfo, clrTypeSyntax), - CSharpInteropTypeSyntax = GetInteropTypeSyntax(nullableTypeInfo), + CSharpInteropTypeSyntax = GetCSInteropTypeSyntax(nullableTypeInfo), CSharpTypeSyntax = clrTypeSyntax, - TypeScriptTypeSyntax = TypeScriptSymbolNameTemplate.ForNullableType(innerTypeInfo.TypeScriptTypeSyntax), - TypeScriptInteropTypeSyntax = TypeScriptSymbolNameTemplate.ForNullableType(innerTypeInfo.TypeScriptInteropTypeSyntax), + TypeScriptTypeSyntax = TypeScriptSymbolNameTemplate.ForNullableType(innerTypeInfo), + TypeScriptInteropTypeSyntax = TypeScriptSymbolNameTemplate.ForNullableType(innerTypeInfo), TypeArgument = innerTypeInfo, + ArgumentInfo = null, IsTaskType = false, IsArrayType = false, IsNullableType = true, @@ -181,14 +248,15 @@ private InteropTypeInfo BuildNullableTypeInfo(JSNullableTypeInfo nullableTypeInf } } - private static TypeSyntax GetInteropTypeSyntax(JSTypeInfo jsTypeInfo) + private static TypeSyntax GetCSInteropTypeSyntax(JSTypeInfo jsTypeInfo) { return jsTypeInfo switch { JSSimpleTypeInfo simpleTypeInfo => simpleTypeInfo.Syntax, JSArrayTypeInfo arrayTypeInfo => arrayTypeInfo.GetTypeSyntax(), JSTaskTypeInfo taskTypeInfo => taskTypeInfo.GetTypeSyntax(), - JSNullableTypeInfo nullableTypeInfo => SyntaxFactory.NullableType(GetInteropTypeSyntax(nullableTypeInfo.ResultTypeInfo)), + JSNullableTypeInfo nullableTypeInfo => SyntaxFactory.NullableType(GetCSInteropTypeSyntax(nullableTypeInfo.ResultTypeInfo)), + JSFunctionTypeInfo functionTypeInfo => functionTypeInfo.GetTypeSyntax().NormalizeWhitespace(), _ => throw new NotSupportedTypeException("Unsupported JSTypeInfo for interop type syntax generation"), } ?? throw new ArgumentException($"Invalid JSTypeInfo of known type '{jsTypeInfo.KnownType}' yielded no syntax"); } @@ -204,10 +272,20 @@ private static TypeSyntax GetJSTypeSyntax(JSTypeInfo jSTypeInfo, TypeSyntax clrT JSNullableTypeInfo { IsValueType: false } nullableTypeInfo => GetJSTypeSyntax(nullableTypeInfo.ResultTypeInfo, clrTypeSyntax).ToString(), JSSpanTypeInfo => throw new NotImplementedException("Span is not yet supported"), JSArraySegmentTypeInfo => throw new NotImplementedException("ArraySegment is not yet supported"), - JSFunctionTypeInfo => throw new NotImplementedException("Func & Action are not yet supported"), + JSFunctionTypeInfo functionTypeInfo => GetFunctionJSMarshalAsTypeArgument(functionTypeInfo), JSInvalidTypeInfo or _ => throw new NotSupportedTypeException(clrTypeSyntax.ToFullString()), }); + static string GetFunctionJSMarshalAsTypeArgument(JSFunctionTypeInfo functionTypeInfo) + { + if (functionTypeInfo.ArgsTypeInfo.Length == 0) + { + return $"JSType.Function"; + } + string[] genericArguments = [.. functionTypeInfo.ArgsTypeInfo.Select(typeInfo => GetJSTypeSyntax(typeInfo, typeInfo.GetTypeSyntax()).ToString())]; + return $"JSType.Function<{string.Join(", ", genericArguments)}>"; + } + static string GetSimpleJSMarshalAsTypeArgument(KnownManagedType knownManagedType) { // Only certain simple types are supported, this method maps them diff --git a/TypeShim.Shared/JSmanagedTypeInfo.cs b/TypeShim.Shared/JSmanagedTypeInfo.cs index d6b4db03..7ee3f4dd 100644 --- a/TypeShim.Shared/JSmanagedTypeInfo.cs +++ b/TypeShim.Shared/JSmanagedTypeInfo.cs @@ -12,7 +12,7 @@ internal abstract record JSTypeInfo(KnownManagedType KnownType) { - public TypeSyntax? GetTypeSyntax() + public TypeSyntax GetTypeSyntax() { return this switch { @@ -33,20 +33,36 @@ internal abstract record JSTypeInfo(KnownManagedType KnownType) SyntaxFactory.TypeArgumentList( SyntaxFactory.SingletonSeparatedList( asti.ElementTypeInfo.Syntax))), - JSTaskTypeInfo { ResultTypeInfo.KnownType: KnownManagedType.Void } => SyntaxFactory.IdentifierName( - SyntaxFactory.Identifier("Task")), + JSTaskTypeInfo { ResultTypeInfo.KnownType: KnownManagedType.Void } => SyntaxFactory.IdentifierName(SyntaxFactory.Identifier("Task")), JSTaskTypeInfo { ResultTypeInfo.KnownType: not KnownManagedType.Void } tti => SyntaxFactory.GenericName( SyntaxFactory.Identifier("Task"), - SyntaxFactory.TypeArgumentList( - SyntaxFactory.SingletonSeparatedList( - tti.ResultTypeInfo.GetTypeSyntax() ?? SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ObjectKeyword))))), - JSNullableTypeInfo nti => SyntaxFactory.NullableType( - nti.ResultTypeInfo.GetTypeSyntax() ?? SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ObjectKeyword))), - JSFunctionTypeInfo fti => throw new NotImplementedException("Function syntax not implemented"), - _ => null, + SyntaxFactory.TypeArgumentList(SyntaxFactory.SingletonSeparatedList(tti.ResultTypeInfo.GetTypeSyntax()))), + JSNullableTypeInfo nti => SyntaxFactory.NullableType(nti.ResultTypeInfo.GetTypeSyntax()), + JSFunctionTypeInfo fti => GetFunctionTypeSyntax(fti), + _ => throw new NotSupportedTypeException($"JS type '{this.GetType()}' with KnownManagedType '{KnownType}' is not supported for type syntax generation"), }; } + private static TypeSyntax GetFunctionTypeSyntax(JSFunctionTypeInfo fti) + { + // - Action: 0 args => Action + // - Action: n args => Action + // - Func: args + return => Func + string identifier = fti.IsAction ? "Action" : "Func"; + + if (fti.ArgsTypeInfo.Length == 0) + { + return SyntaxFactory.IdentifierName(SyntaxFactory.Identifier(identifier)); + } + + SeparatedSyntaxList typeArgs = SyntaxFactory.SeparatedList( + fti.ArgsTypeInfo.Select(x => x.GetTypeSyntax())); + + return SyntaxFactory.GenericName( + SyntaxFactory.Identifier(identifier), + SyntaxFactory.TypeArgumentList(typeArgs)); + } + public static JSTypeInfo CreateJSTypeInfoForTypeSymbol(ITypeSymbol type) { string fullTypeName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); @@ -192,8 +208,8 @@ public static JSTypeInfo CreateJSTypeInfoForTypeSymbol(ITypeSymbol type) case ITypeSymbol when fullTypeName == Constants.ActionGlobal: return new JSFunctionTypeInfo(true, Array.Empty()); case INamedTypeSymbol actionType when fullTypeName.StartsWith(Constants.ActionGlobal, StringComparison.Ordinal): - JSSimpleTypeInfo?[] argumentTypes = [.. actionType.TypeArguments.Select(arg => CreateJSTypeInfoForTypeSymbol(arg) as JSSimpleTypeInfo)]; - if (argumentTypes.Any(x => x is null)) + JSTypeInfo?[] argumentTypes = [.. actionType.TypeArguments.Select(CreateJSTypeInfoForTypeSymbol)]; + if (argumentTypes.Length > 3 || argumentTypes.Any(x => x is not JSSimpleTypeInfo and not JSNullableTypeInfo { IsValueType: false, ResultTypeInfo: JSSimpleTypeInfo })) { return new JSInvalidTypeInfo(); } @@ -201,8 +217,8 @@ public static JSTypeInfo CreateJSTypeInfoForTypeSymbol(ITypeSymbol type) // function case INamedTypeSymbol funcType when fullTypeName.StartsWith(Constants.FuncGlobal, StringComparison.Ordinal): - JSSimpleTypeInfo?[] signatureTypes = [.. funcType.TypeArguments.Select(argName => CreateJSTypeInfoForTypeSymbol(argName) as JSSimpleTypeInfo)]; - if (signatureTypes.Any(x => x is null)) + JSTypeInfo?[] signatureTypes = [.. funcType.TypeArguments.Select(CreateJSTypeInfoForTypeSymbol)]; + if (signatureTypes.Length > 4 || signatureTypes.Any(x => x is not JSSimpleTypeInfo and not JSNullableTypeInfo { IsValueType: false, ResultTypeInfo: JSSimpleTypeInfo })) { return new JSInvalidTypeInfo(); } @@ -240,7 +256,7 @@ internal sealed record JSTaskTypeInfo(JSTypeInfo ResultTypeInfo) : JSTypeInfo(Kn internal sealed record JSNullableTypeInfo(JSTypeInfo ResultTypeInfo, bool IsValueType) : JSTypeInfo(KnownManagedType.Nullable); -internal sealed record JSFunctionTypeInfo(bool IsAction, JSSimpleTypeInfo[] ArgsTypeInfo) : JSTypeInfo(IsAction ? KnownManagedType.Action : KnownManagedType.Function); +internal sealed record JSFunctionTypeInfo(bool IsAction, JSTypeInfo[] ArgsTypeInfo) : JSTypeInfo(IsAction ? KnownManagedType.Action : KnownManagedType.Function); internal enum KnownManagedType : int diff --git a/TypeShim.Shared/TypeScriptSymbolNameTemplate.cs b/TypeShim.Shared/TypeScriptSymbolNameTemplate.cs index 128ef70e..d3833fb7 100644 --- a/TypeShim.Shared/TypeScriptSymbolNameTemplate.cs +++ b/TypeShim.Shared/TypeScriptSymbolNameTemplate.cs @@ -1,34 +1,23 @@ - -using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Linq; +using System.Collections.Generic; +using System.Text; namespace TypeShim.Shared; +internal sealed record TypeScriptFunctionParameterTemplate(string Name, TypeScriptSymbolNameTemplate TypeTemplate); + internal sealed class TypeScriptSymbolNameTemplate { - internal required string Template { get; init; } - internal required TypeScriptSymbolNameTemplate? InnerTemplate { get; init; } + internal string Template { get; init; } = null!; + internal Dictionary InnerTypes { get; init; } = []; - private const string InnerPlaceholder = "{INNER_PLACEHOLDER}"; - private const string SuffixPlaceholder = "{SUFFIX_PLACEHOLDER}"; - - internal string Render(string suffix = "") - { - string template = Template; - if (InnerTemplate is not null) - { - string inner = InnerTemplate.Render(suffix); - template = template.Replace(InnerPlaceholder, inner); - } - - return template.Replace(SuffixPlaceholder, suffix); - } internal static TypeScriptSymbolNameTemplate ForUserType(string originalTypeSyntax) { return new TypeScriptSymbolNameTemplate { - Template = $"{originalTypeSyntax}{SuffixPlaceholder}", - InnerTemplate = null + Template = originalTypeSyntax, }; } @@ -37,34 +26,73 @@ internal static TypeScriptSymbolNameTemplate ForSimpleType(string typeName) return new TypeScriptSymbolNameTemplate { Template = typeName, - InnerTemplate = null }; } - internal static TypeScriptSymbolNameTemplate ForArrayType(TypeScriptSymbolNameTemplate innerTemplate) + internal static TypeScriptSymbolNameTemplate ForArrayType(InteropTypeInfo innerType) { return new TypeScriptSymbolNameTemplate { - Template = $"Array<{InnerPlaceholder}>", - InnerTemplate = innerTemplate + Template = "Array<{TElement}>", + InnerTypes = { { "{TElement}", innerType } } }; } - internal static TypeScriptSymbolNameTemplate ForPromiseType(TypeScriptSymbolNameTemplate? innerTemplate) + internal static TypeScriptSymbolNameTemplate ForPromiseType(InteropTypeInfo? innerType) { + if (innerType == null) + { + return new TypeScriptSymbolNameTemplate + { + Template = "Promise", + }; + } return new TypeScriptSymbolNameTemplate { - Template = innerTemplate != null ? $"Promise<{InnerPlaceholder}>" : "Promise", - InnerTemplate = innerTemplate + Template = "Promise<{TValue}>", + InnerTypes = { { "{TValue}", innerType } } }; } - internal static TypeScriptSymbolNameTemplate ForNullableType(TypeScriptSymbolNameTemplate innerTemplate) + internal static TypeScriptSymbolNameTemplate ForNullableType(InteropTypeInfo innerType) + { + string template = innerType.IsDelegateType() ? "({TNullableValue}) | null" : "{TNullableValue} | null"; + return new TypeScriptSymbolNameTemplate + { + Template = template, + InnerTypes = { { "{TNullableValue}", innerType } } + }; + } + + internal static TypeScriptSymbolNameTemplate ForDelegateType(DelegateArgumentInfo argumentInfo) { + Dictionary paramTypeDict = [.. argumentInfo.ParameterTypes.Select((typeInfo, i) => new KeyValuePair($"{{TArg{i}}}", typeInfo))]; + KeyValuePair returnTypeKvp = new("{TReturn}", argumentInfo.ReturnType); + + StringBuilder templateBuilder = new(); + templateBuilder.Append('('); + int i = 0; + foreach (KeyValuePair typeInfo in paramTypeDict) + { + if (i > 0) templateBuilder.Append(", "); + templateBuilder.Append("arg").Append(i).Append(": ").Append(typeInfo.Key); + i++; + } + templateBuilder.Append(") => ").Append(returnTypeKvp.Key); return new TypeScriptSymbolNameTemplate { - Template = $"{InnerPlaceholder} | null", - InnerTemplate = innerTemplate + Template = templateBuilder.ToString(), + InnerTypes = [..paramTypeDict, returnTypeKvp] }; } } + +public static class DictionaryExtensions +{ + // Enables collection expressions for Dictionary like newdict = [..dict, kvp] + public static Dictionary Add(this Dictionary dict, KeyValuePair keyValuePair) where TKey : notnull + { + dict[keyValuePair.Key] = keyValuePair.Value; + return dict; + } +} \ No newline at end of file