Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/guide/declarations.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,40 @@ export namespace Class {

:::

## Generic Methods

JavaScript does not have generic functions, so Bootsharp projects an exported generic method as a concrete overload for each user type that satisfies the method's type parameter constraint, suffixing each with `Of...` derived from the bound type's name. Only a single type parameter, constrained to a user type, is expanded.

::: code-group

```csharp [Class.cs]
public interface IShape {}
public class Circle : IShape {}
public class Square : IShape {}

public class Class
{
[Export]
public static T CreateShape<T> () where T : IShape
{
if (typeof(T) == typeof(Circle)) return new Circle();
if (typeof(T) == typeof(Square)) return new Square();
}
}
```

```ts [index.g.d.mts]
export interface Circle {}
export interface Square {}

export namespace Class {
export function createShapeOfCircle(): Circle;
export function createShapeOfSquare(): Square;
}
```

:::

## Default Arguments

C# method parameters with default values are emitted as optional TypeScript parameters using the `?:` syntax, letting callers omit them at the call site:
Expand Down
45 changes: 45 additions & 0 deletions src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,51 @@ public class Class
DoesNotContain("Foo");
}

[Fact]
public void GeneratesSupportedGenericMethods ()
{
AddAssembly(With(
"""
public interface IShape {}
public class Circle : IShape { public double Radius { get; set; } }
public class Square : IShape { public double Side { get; set; } }

public class Class
{
[Export] public static T Make<T> () where T : IShape => default!;
[Export] public static void Take<T> (T shape) where T : IShape {}
[Export] public static void Mix<T> (T shape, int n) where T : IShape {}
[Export] public static T? EchoNull<T> (T? shape) where T : class, IShape => shape;
}
"""));
Execute();
Contains("Class_MakeOfCircle () => Instances.Export(global::Class.Make<global::Circle>())");
Contains("Class_MakeOfSquare () => Instances.Export(global::Class.Make<global::Square>())");
Contains("Class_TakeOfCircle (int shape) => global::Class.Take<global::Circle>(Instances.Resolve<global::Circle>(shape))");
Contains("Class_TakeOfSquare (int shape) => global::Class.Take<global::Square>(Instances.Resolve<global::Square>(shape))");
Contains("Class_MixOfCircle (int shape, global::System.Int32 n) => global::Class.Mix<global::Circle>(Instances.Resolve<global::Circle>(shape), n)");
Contains("Class_EchoNullOfCircle (int shape) => Instances.Export(global::Class.EchoNull<global::Circle>(Instances.Resolve<global::Circle>(shape)))");
}

[Fact]
public void GeneratesGenericMethodsForCompatibleTypesInOtherAssemblies ()
{
AddAssembly("Contracts.dll", With("public interface IShape {}"));
AddAssembly("Shapes.dll", With(
"""
public class Circle : IShape { public double Radius { get; set; } }
public class Square : IShape { public double Side { get; set; } }

public class Class
{
[Export] public static T Make<T> () where T : IShape => default!;
}
"""));
Execute();
Contains("Class_MakeOfCircle () => Instances.Export(global::Class.Make<global::Circle>())");
Contains("Class_MakeOfSquare () => Instances.Export(global::Class.Make<global::Square>())");
}

[Fact]
public void DoesNotSerializeTypesThatShouldNotBeSerialized ()
{
Expand Down
22 changes: 22 additions & 0 deletions src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,28 @@ export interface GenericClass2<T1, T2> {
""");
}

[Fact]
public void GeneratedForGenericMethods ()
{
AddAssembly(With(
"""
public interface IShape {}
public class Circle : IShape { public double Radius { get; set; } }
public class Square : IShape { public double Side { get; set; } }

public class Class
{
[Export] public static T Make<T> () where T : IShape => default!;
[Export] public static void Take<T> (T shape) where T : IShape {}
}
"""));
Execute();
Contains("export function makeOfCircle(): Circle;");
Contains("export function makeOfSquare(): Square;");
Contains("export function takeOfCircle(shape: Circle): void;");
Contains("export function takeOfSquare(shape: Square): void;");
}

[Fact]
public void GeneratesForDelegates ()
{
Expand Down
98 changes: 84 additions & 14 deletions src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -705,31 +705,101 @@ [Export] public static void Start (string title, string info, double progress) {
"""
export const Class = {
foo: (a) => exports.Class_Foo(a),
fooWithA: (a) => exports.Class_Foo(a),
fooWithB: (b, a) => exports.Class_Foo(b, a),
fooWithA: (a) => exports.Class_FooWithA(a),
fooWithB: (b, a) => exports.Class_FooWithB(b, a),
bar: (x) => exports.Class_Bar(x),
barWithName: (x, name) => exports.Class_Bar(x, name),
barWithName: (x, name) => exports.Class_BarWithName(x, name),
baz: (x, y) => exports.Class_Baz(x, y),
bazWithXAndY: (x, y) => exports.Class_Baz(x, y),
bazWithXAndY: (x, y) => exports.Class_BazWithXAndY(x, y),
qux: (a) => exports.Class_Qux(a),
quxWithB: (a, b) => exports.Class_Qux(a, b),
quxWithBAndC: (a, b, c) => exports.Class_Qux(a, b, c),
quxWithB: (a, b) => exports.Class_QuxWithB(a, b),
quxWithBAndC: (a, b, c) => exports.Class_QuxWithBAndC(a, b, c),
x: (x, y) => exports.Class_X(x, y),
xWithStringAndInt32: (x, y) => exports.Class_X(x, y),
xWithInt32AndString: (x, y) => exports.Class_X(x, y),
xWithStringAndInt32: (x, y) => exports.Class_XWithStringAndInt32(x, y),
xWithInt32AndString: (x, y) => exports.Class_XWithInt32AndString(x, y),
bob: (x, y, z) => exports.Class_Bob(x, y, z),
bobWithQ: (x, y, q) => exports.Class_Bob(x, y, q),
bobWithQ: (x, y, q) => exports.Class_BobWithQ(x, y, q),
change: (progress) => exports.Class_Change(progress),
changeWithInfo: (info) => exports.Class_Change(info),
changeWithProgressAndInfo: (progress, info) => exports.Class_Change(progress, info),
changeWithInfo: (info) => exports.Class_ChangeWithInfo(info),
changeWithProgressAndInfo: (progress, info) => exports.Class_ChangeWithProgressAndInfo(progress, info),
start: (title) => exports.Class_Start(title),
startWithInfo: (title, info) => exports.Class_Start(title, info),
startWithProgress: (title, progress) => exports.Class_Start(title, progress),
startWithInfoAndProgress: (title, info, progress) => exports.Class_Start(title, info, progress)
startWithInfo: (title, info) => exports.Class_StartWithInfo(title, info),
startWithProgress: (title, progress) => exports.Class_StartWithProgress(title, progress),
startWithInfoAndProgress: (title, info, progress) => exports.Class_StartWithInfoAndProgress(title, info, progress)
};
""");
}

[Fact]
public void GeneratesSupportedGenericMethods ()
{
AddAssembly(With(
"""
public interface IShape {}
public class Circle : IShape { public double Radius { get; set; } }
public record Square : IShape { public double Side { get; set; } }

public class Class
{
[Export] public static T Make<T> () where T : IShape => default!;
[Export] public static void Take<T> (T shape) where T : IShape {}
}
"""));
Execute();
Contains("makeOfCircle: () =>");
Contains("makeOfSquare: () =>");
Contains("takeOfCircle: (shape) =>");
Contains("takeOfSquare: (shape) =>");
Contains("exports.Class_MakeOfCircle()");
Contains("exports.Class_MakeOfSquare()");
Contains("exports.Class_TakeOfCircle(");
Contains("exports.Class_TakeOfSquare(");
}

[Fact]
public void DiscardsUnsupportedGenericMethods ()
{
AddAssembly(With(
"""
public interface IShape {}
public class Circle : IShape { public double Radius { get; set; } }
public static class Box<T> { [Export] public static void Stored (T item) {} }
public class Class
{
[Export] public static void Pair<T1, T2> () where T1 : IShape where T2 : IShape {}
[Export] public static void Take<T> (T shape) where T : IShape {}
[Export] public static void Many<T> (List<T> items) where T : IShape {}
[Export] public static void Free<T> () {}
[Export] public static void Real () {}
}
"""));
Execute();
Contains("real:");
Contains("takeOfCircle:");
DoesNotContain("stored"); // method declared on a generic type
DoesNotContain("pair"); // multiple type parameters
DoesNotContain("many"); // type parameter used nested
DoesNotContain("free"); // type parameter not constrained to a user type
}

[Fact]
public void DiscardsGeneratedTypesFromGenericCandidates ()
{
AddAssembly(With(
"""
public interface IShape {}
public class Circle : IShape { public double Radius { get; set; } }
namespace Bootsharp.Generated { public class JS_Import_Leaked : global::IShape {} }
public class Class
{
[Export] public static T Make<T> () where T : IShape => default!;
}
"""));
Execute();
Contains("makeOfCircle:");
DoesNotContain("makeOfJS_Import_Leaked");
}

[Fact]
public void RespectsPrefsInStatics ()
{
Expand Down
11 changes: 8 additions & 3 deletions src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public static bool IsUserAssembly (string assemblyName) =>

public static bool IsUserType (Type type)
{
if (type.Namespace?.StartsWith("Bootsharp.Generated") == true) return false;
if (Preferences.IsSpecialized(type)) return true;
if (IsDelegate(type)) return true;
if (type.IsArray) return false;
Expand Down Expand Up @@ -79,10 +80,14 @@ public bool HasBase (Type clr, [NotNullWhen(true)] out TypeMeta? bs)
return bs != null && IsUserType(bs.Clr);
}

public TypeMeta Get (Type clr)
public TypeMeta First (Type clr)
{
var def = clr.IsGenericType ? clr.GetGenericTypeDefinition() : clr;
return types.First(t => (t.Clr.IsGenericType ? t.Clr.GetGenericTypeDefinition() : t.Clr) == def);
return types.First(t => IsSameType(t.Clr, clr));
}

public bool Has (Type clr)
{
return types.Any(t => IsSameType(t.Clr, clr));
}
}
}
4 changes: 4 additions & 0 deletions src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ static bool IsDictionary (Type type) =>
type.GetGenericTypeDefinition().FullName == typeof(IReadOnlyDictionary<,>).FullName);
}

public static bool IsSameType (Type a, Type b) =>
(a.IsGenericType ? a.GetGenericTypeDefinition() : a) ==
(b.IsGenericType ? b.GetGenericTypeDefinition() : b);

public static NullabilityInfo GetNullity (PropertyInfo prop) =>
FixNullity(new NullabilityInfoContext().Create(prop), prop.CustomAttributes, prop.DeclaringType);
public static NullabilityInfo GetNullity (ParameterInfo param) =>
Expand Down
76 changes: 76 additions & 0 deletions src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Reflection;

namespace Bootsharp.Publish;

/// <summary>
/// Rewrites <see cref="MethodMeta"/> associated with the generic methods expanding them into a concrete
/// overload for each user type compatible with the method's type parameter constraint.
/// </summary>
internal static class GenericDisambiguator
{
public static void Disambiguate (TypeMeta[] types)
{
foreach (var surface in types.OfType<SurfaceMeta>())
foreach (var method in surface.Members.OfType<MethodMeta>().ToArray())
if (method.IK == InteropKind.Export && method.Info.ContainsGenericParameters)
Disambiguate(surface, method, types);
}

private static void Disambiguate (SurfaceMeta surf, MethodMeta meth, TypeMeta[] types)
{
surf.MemberList.Remove(meth);
foreach (var expanded in Expand(meth, types))
surf.MemberList.Add(expanded);
}

private static IEnumerable<MethodMeta> Expand (MethodMeta meth, TypeMeta[] types)
{
if (!meth.Info.IsGenericMethodDefinition) yield break; // ignore methods declared on a generic type
if (meth.Info.GetGenericArguments() is not [{ } param]) yield break; // only single <T> supported
if (param.GetGenericParameterConstraints().FirstOrDefault(IsUserType) is not { } ct) yield break;
Comment thread
elringus marked this conversation as resolved.
if (HasNestedGeneric(meth.Info)) yield break; // type parameter nested in another type can't be rewritten
foreach (var compatible in GetCompatible(ct, types))
yield return CloseGeneric(meth, compatible);
Comment thread
elringus marked this conversation as resolved.
}

private static bool HasNestedGeneric (MethodInfo meth) => meth
.GetParameters().Select(p => p.ParameterType).Prepend(meth.ReturnType)
.Any(t => t.ContainsGenericParameters && !t.IsGenericMethodParameter);

private static IEnumerable<TypeMeta> GetCompatible (Type ct, TypeMeta[] types) => types
.Where(t => t is InstanceMeta or SerializedObjectMeta && !t.Clr.IsAbstract && ct.IsAssignableFrom(t.Clr))
.DistinctBy(t => t.Clr);

private static MethodMeta CloseGeneric (MethodMeta meth, TypeMeta closeType)
{
var closed = meth.Info.MakeGenericMethod(closeType.Clr);
return new MethodMeta(closed) {
Surf = meth.Surf,
IK = meth.IK,
Name = $"{meth.Name}<{closeType.Syntax}>",
Endpoint = $"{meth.Name}Of{closeType.Id}",
JSName = $"{meth.JSName}Of{closeType.Clr.Name}",
Comment thread
elringus marked this conversation as resolved.
Args = meth.Args.Zip(closed.GetParameters(), RewriteArg).ToArray(),
Return = meth.Info.ReturnType.IsGenericMethodParameter ? RewriteValue(meth.Return) : meth.Return,
Void = meth.Void,
Async = meth.Async
};

ArgumentMeta RewriteArg (ArgumentMeta arg, ParameterInfo param)
{
if (!arg.Info.ParameterType.IsGenericMethodParameter) return arg;
Comment thread
elringus marked this conversation as resolved.
var value = RewriteValue(arg.Value);
return new ArgumentMeta(param) { Name = arg.Name, JSName = arg.JSName, Value = value };
}

ValueMeta RewriteValue (ValueMeta value) => value with {
Type = closeType,
TypeSyntax = value.Nullable ? $"{closeType.Syntax}?" : closeType.Syntax
};
}

extension (SurfaceMeta srf)
{
private IList<MemberMeta> MemberList => (IList<MemberMeta>)srf.Members;
}
}
4 changes: 4 additions & 0 deletions src/cs/Bootsharp.Publish/Common/Inspection/Meta/MemberMeta.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ internal record MethodMeta (MethodInfo Info) : MemberMeta
/// </summary>
public override MethodInfo Info { get; } = Info;
/// <summary>
/// Identifier of the generated interop endpoint.
/// </summary>
public required string Endpoint { get; init; }
/// <summary>
/// Arguments of the method.
/// </summary>
public required IReadOnlyList<ArgumentMeta> Args { get; init; }
Expand Down
Loading
Loading