Skip to content

Commit 9372094

Browse files
committed
fix
1 parent 3123124 commit 9372094

12 files changed

Lines changed: 127 additions & 59 deletions

File tree

docs/guide/specialization.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ The `[SpecializeImport]` attribute accepts optional `CS`, `JS`, `JSCtor` and `De
6060
Decl: "[Symbol.iterator](): IterableIterator<T>;")]
6161
```
6262

63-
The `CS` snippet can contain `$it` markers — they will be replaced with the fully-qualified C# syntax of the specialized instance. This allows referencing the concrete specialized instances in the proxy when the specialization is applied to a base class.
63+
The `CS` snippet can contain `$full` markers — they will be replaced with the fully-qualified type name of the specialized instance. This allows referencing the concrete specialized instances in the proxy when the specialization is applied to a base class.
64+
65+
The `Decl` snippet can contain `$full`, `$name` and `$T{I}` markers — first is the same as CS one, but in TypeScript context, name is the short type name and `T` is the fully-qualified name of the generic type argument with the `{I}` inde (if any), for example `$T{0}` is replaced with the first generic argument.
6466

6567
When `Decl` value starts with `export ` — the content will replace the entire TypeScript declaration of the type, instead of splicing it into the bottom of the default type declaration.
6668

src/cs/Bootsharp.Common/Attributes/SpecializeImportAttribute.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace Bootsharp;
1313
/// </param>
1414
/// <param name="CS">
1515
/// Raw snippet spliced into the generated C# import proxy class body.
16-
/// All occurrences of '$it' are replaced with the fully-qualified C# syntax of the specialized instance type.
16+
/// All occurrences of '$full' are replaced with the fully-qualified type name of the specialized instance type.
1717
/// </param>
1818
/// <param name="JS">
1919
/// Raw snippet spliced into the generated JavaScript export proxy class body.
@@ -24,6 +24,10 @@ namespace Bootsharp;
2424
/// <param name="Decl">
2525
/// Raw snippet spliced into the generated TypeScript declaration.
2626
/// When starts with 'export ' will instead replace the whole declaration.
27+
/// Occurrences of '$name' are replaced with the name of the specialized instance type.
28+
/// Occurrences of '$full' are replaced with the fully-qualified name of the specialized instance type.
29+
/// When the instance type is generic, occurrences of '$T{I}' are replaced with the fully-qualified names
30+
/// of the generic type arguments with the {I} index, starting with 0 (eg, '$T0' is the first generic arg).
2731
/// </param>
2832
[AttributeUsage(AttributeTargets.Class)]
2933
public sealed class SpecializeImportAttribute (Type Clr, string? CS = null, string? JS = null,

src/cs/Bootsharp.Publish.Test/GenerateCS/CSInstanceTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ public void GeneratesForCustomSpecializationOfBaseClass ()
287287
public abstract class Event<T>;
288288
public sealed class IntEvent : Event<int>;
289289
290-
[SpecializeImport(typeof(Event<>), CS: "protected override object Unwrap () => new $it();")]
290+
[SpecializeImport(typeof(Event<>), CS: "protected override object Unwrap () => new $full();")]
291291
public abstract class EventImport<T> (int id) : SpecializedImport(id);
292292
293293
[SpecializeExport(typeof(Event<>))]

src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,25 @@ export namespace Foo {
7171
""");
7272
}
7373

74+
[Fact]
75+
public void TypeNestedUnderInstancedDeclaredUnderNamespace ()
76+
{
77+
AddAssembly(
78+
With("public class Foo { public record Bar; public int Value { get; } }"),
79+
WithClass("[Export] public static Foo Get (Foo.Bar b) => default;"));
80+
Execute();
81+
Contains(
82+
"""
83+
export interface Foo {
84+
readonly value: number;
85+
}
86+
export namespace Foo {
87+
export type Bar = Readonly<{
88+
}>;
89+
}
90+
""");
91+
}
92+
7493
[Fact]
7594
public void CrawledTypeDoesNotOverrideSpecialized ()
7695
{
@@ -358,6 +377,22 @@ export interface Interface {
358377
""");
359378
}
360379

380+
[Fact]
381+
public void ExtendsClosedGenericWithClosedArguments ()
382+
{
383+
AddAssembly(
384+
With("public interface IBar<T> { T Get (); }"),
385+
With("public class Foo : IBar<int> { public int Get () => 0; }"),
386+
WithClass("[Export] public static Foo UseFoo (IBar<int> bar) => default;"));
387+
Execute();
388+
Contains(
389+
"""
390+
export interface Foo extends IBar<number> {
391+
get(): number;
392+
}
393+
""");
394+
}
395+
361396
[Fact]
362397
public void GeneratedForSpecializedTypes ()
363398
{
@@ -400,6 +435,27 @@ public class Class { [Export] public static Foo Get () => default; }
400435
DoesNotContain("export interface Foo");
401436
}
402437

438+
[Fact]
439+
public void SpecializedDeclarationSubstitutesTemplates ()
440+
{
441+
AddAssembly(With(
442+
"""
443+
public delegate void Handler (string arg);
444+
public abstract class Event<T> where T : System.Delegate;
445+
public sealed class HandlerEvent : Event<Handler>;
446+
[SpecializeImport(typeof(Event<>), Decl: "export interface $name { broadcast: $T0; }")]
447+
public abstract class EventImport<T> (int id) : SpecializedImport(id) where T : System.Delegate
448+
{
449+
public abstract void Subscribe (T handler);
450+
}
451+
[SpecializeExport(typeof(Event<>))]
452+
public sealed class EventExport<T> (Event<T> it) : SpecializedExport(it) where T : System.Delegate;
453+
public class Class { [Export] public static HandlerEvent Get () => default; }
454+
"""));
455+
Execute();
456+
Contains("export interface HandlerEvent { broadcast: Handler; }");
457+
}
458+
403459
[Fact]
404460
public void GeneratedForCollections ()
405461
{

src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public sealed class {{it.Proxy.Id}} (int id) : {{sp.Import.Syntax}}(id)
9898
{
9999
~{{it.Proxy.Id}}() => Instances.DisposeImported(_id);
100100
101-
{{Fmt([..it.Members.Select(EmitMemberImport), sp.CS?.Replace("$it", it.Syntax)])}}
101+
{{Fmt([..it.Members.Select(EmitMemberImport), sp.CS?.Replace("$full", it.Syntax)])}}
102102
}
103103
""";
104104

src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,32 +33,31 @@ public string Generate (JSModule module)
3333
}
3434

3535
private string EmitImports (JSModule md) => Fmt([
36-
$$"""import type * as $bcl from "{{md.To("bcl/index")}}";""",
36+
$"""import type * as $bcl from "{md.To("bcl/index")}";""",
3737
..mds.GetImported(md).Select(imp =>
3838
$"""import type * as {imp.Alias} from "{md.ToMd(imp.Path)}";""")
3939
], 0);
4040

4141
private void DeclareNode (JSNode node)
4242
{
43-
var surf = node.Types.FirstOrDefault(s => s is SurfaceMeta and not InstanceMeta);
44-
var wrap = surf != null || node.Children.Count > 0;
45-
if (wrap)
46-
{
47-
if (surf != null) doc.Type(surf);
48-
bld.Enter($"export namespace {node.Name} {{");
49-
}
43+
var surfaces = new List<SurfaceMeta>();
5044
foreach (var type in node.Types)
5145
// Dedup by CLR to discard the other side of a bidirectional (export+import)
5246
// instance surface and closed generic variants (all produce same open type).
5347
if (declared.Add(OpenGeneric(type.Clr)))
5448
if (type is SerializedEnumMeta enu) DeclareEnum(enu);
55-
else if (type is SerializedObjectMeta o) DeclareSerialized(o);
56-
else if (type is DelegateMeta d) DeclareDelegate(d);
49+
else if (type is SerializedObjectMeta ser) DeclareSerialized(ser);
50+
else if (type is DelegateMeta del) DeclareDelegate(del);
5751
else if (type is InstanceMeta it) DeclareInstance(it);
58-
else if (type is SurfaceMeta srf) DeclareSurface(srf);
52+
else if (type is SurfaceMeta srf) surfaces.Add(srf);
53+
if (surfaces.Count == 0 && node.Children.Count == 0) return;
54+
if (surfaces.Count > 0) doc.Type(surfaces[0]);
55+
bld.Enter($"export namespace {node.Name} {{");
56+
foreach (var surface in surfaces)
57+
DeclareSurface(surface);
5958
foreach (var child in node.Children)
6059
DeclareNode(child);
61-
if (wrap) bld.Exit("}");
60+
bld.Exit("}");
6261
}
6362

6463
private void DeclareEnum (SerializedEnumMeta enu)
@@ -77,7 +76,7 @@ private void DeclareEnum (SerializedEnumMeta enu)
7776
private void DeclareSerialized (SerializedObjectMeta obj)
7877
{
7978
doc.Type(obj);
80-
var ext = spec.Types.HasBase(obj.Clr, out var bs) ? $"{ts.BuildFullName(bs.Clr)} & " : "";
79+
var ext = spec.Types.HasBase(obj.Clr, out var bs) ? $"{ts.BuildRef(bs.Clr)} & " : "";
8180
bld.Enter($$"""export type {{ts.BuildName(obj.Clr)}} = {{ext}}Readonly<{""");
8281
foreach (var prop in obj.Properties.Where(p => ShouldDeclareOn(obj.Clr, p.Info)))
8382
{
@@ -98,25 +97,33 @@ private void DeclareDelegate (DelegateMeta del)
9897
private void DeclareInstance (InstanceMeta it)
9998
{
10099
doc.Type(it);
101-
if (it.Proxy is SpecializedProxy { Decl: { } sp } && sp.StartsWith("export "))
102-
{
103-
bld.Line(sp);
104-
return;
105-
}
100+
if (DeclareSpecialized(out var specialized)) return;
106101
bld.Enter($$"""export interface {{ts.BuildName(it.Clr)}}{{BuildExtensions()}} {""");
107102
foreach (var member in it.Members.Where(m => ShouldDeclareOn(it.Clr, m.Info)))
108103
if (member is EventMeta evt) DeclareEvent(evt);
109104
else if (member is PropertyMeta prop) DeclareProperty(prop);
110105
else if (member is MethodMeta method) DeclareMethod(method);
111-
if (it.Proxy is SpecializedProxy { Decl: { } decl }) bld.Line(decl);
106+
if (specialized != null) bld.Line(specialized);
112107
bld.Exit("}");
113108
109+
bool DeclareSpecialized (out string? decl)
110+
{
111+
if (string.IsNullOrEmpty(decl = (it.Proxy as SpecializedProxy)?.Decl)) return false;
112+
decl = decl.Replace("$name", ts.BuildName(it.Clr)).Replace("$full", ts.BuildRef(it.Clr));
113+
var args = (it.Proxy as SpecializedProxy)!.Import.Clr.GetGenericArguments();
114+
for (var i = 0; i < args.Length; i++)
115+
decl = decl.Replace($"$T{i}", ts.BuildRef(args[i]));
116+
var replaces = decl.StartsWith("export ");
117+
if (replaces) bld.Line(decl);
118+
return replaces;
119+
}
120+
114121
string BuildExtensions ()
115122
{
116123
if (it.Proxy is SpecializedProxy) return ""; // specialized surfaces are self-contained
117124
var ext = it.Clr.GetInterfaces().Where(i => IsUserType(i) && spec.Types.Has(i)).ToList();
118125
if (spec.Types.HasBase(it.Clr, out var bs)) ext.Insert(0, bs.Clr);
119-
return ext.Count == 0 ? "" : $" extends {string.Join(", ", ext.Select(ts.BuildFullName))}";
126+
return ext.Count == 0 ? "" : $" extends {string.Join(", ", ext.Select(ts.BuildRef))}";
120127
}
121128
122129
void DeclareEvent (EventMeta evt)

src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ public void EnterModule (JSModule module)
1616

1717
public string BuildName (Type type)
1818
{
19-
var full = BuildFullName(type);
19+
var full = BuildRef(OpenGeneric(type));
2020
var dotIdx = full.LastIndexOf('.');
2121
return dotIdx > 0 ? full[(dotIdx + 1)..] : full;
2222
}
2323

24-
public string BuildFullName (Type type)
24+
public string BuildRef (Type type)
2525
{
26-
return Build(OpenGeneric(type), null);
26+
return Build(type, null);
2727
}
2828

2929
public string BuildArg (ParameterInfo param)

src/cs/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22

33
<PropertyGroup>
4-
<Version>0.9.0-alpha.21</Version>
4+
<Version>0.9.0-alpha.28</Version>
55
<Authors>Elringus</Authors>
66
<PackageTags>javascript typescript ts js wasm node deno bun interop codegen</PackageTags>
77
<PackageProjectUrl>https://bootsharp.com</PackageProjectUrl>

src/js/test/cs/Test.Library/Modules/BidirectionalCS.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Test.Library;
55
public class BidirectionalCS : IBidirectional
66
{
77
public event Action<IBidirectional?>? OnBiChanged;
8-
public SpecialBiHandlerEvent OnSpecial { get; } = new();
8+
public IBidirectional.SpecialEvent OnSpecial { get; } = new();
99

1010
public IBidirectional? Bi { get; set => NotifyChanged(field = value); } = null!;
1111

src/js/test/cs/Test.Library/Modules/IBidirectional.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,29 @@ namespace Test.Library;
44

55
public interface IBidirectional
66
{
7+
public delegate void SpecialHandler (IBidirectional? bi);
8+
public sealed class SpecialEvent : Event<SpecialHandler>
9+
{
10+
public IBidirectional? Last { get; private set; }
11+
12+
public void Broadcast (IBidirectional? bi)
13+
{
14+
Last = bi;
15+
foreach (var handler in Handlers)
16+
handler(bi);
17+
}
18+
19+
public override void Replay (SpecialHandler handler)
20+
{
21+
if (Last is { } last)
22+
handler(last);
23+
}
24+
}
25+
726
event Action<IBidirectional?>? OnBiChanged;
827

928
IBidirectional? Bi { get; set; }
10-
SpecialBiHandlerEvent OnSpecial { get; }
29+
SpecialEvent OnSpecial { get; }
1130

1231
IBidirectional? EchoBi (IBidirectional? bi);
1332
}

0 commit comments

Comments
 (0)