From a768a2a8f3c574b091b685ab2f47aa19d27286cc Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Thu, 21 May 2026 18:11:58 +0300 Subject: [PATCH 1/2] implement --- samples/bench/bootsharp/package.json | 2 +- .../GenerateJS/JSModuleTest.cs | 61 +++++++++++++++++++ .../Common/Global/GlobalText.cs | 6 ++ .../Inspection/OverloadDisambiguator.cs | 56 +++++++++++++++++ .../Common/Inspection/TypeInspector.cs | 1 + src/cs/Directory.Build.props | 2 +- 6 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs diff --git a/samples/bench/bootsharp/package.json b/samples/bench/bootsharp/package.json index 92794570..4d5f0987 100644 --- a/samples/bench/bootsharp/package.json +++ b/samples/bench/bootsharp/package.json @@ -3,7 +3,7 @@ "type": "module", "exports": { ".": "./bin/bootsharp/index.mjs", - "./*": "./bin/bootsharp/generated/*.g.mjs" + "./*": "./bin/bootsharp/generated/modules/*.g.mjs" }, "browser": { "node:fs": false, diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs index b7ee5b7f..fb65c9dd 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs @@ -669,6 +669,67 @@ public void DoesNotEmitModulesForBclTypes () DoesNotContain("../imports.g.mjs", "system"); } + [Fact] + public void OverloadedMethodsAreDisambiguated () + { + AddAssembly(With( + """ + public class Class + { + [Export] public static void Foo (int a) {} + [Export] public static void Foo (string a) {} + [Export] public static void Foo (double b, double a) {} + [Export] public static void Bar (int x) {} + [Export] public static void Bar (int x, string name) {} + [Export] public static void Baz (int x, int y) {} + [Export] public static void Baz (int x, string y) {} + [Export] public static void Qux (int a) {} + [Export] public static void Qux (int a, int b) {} + [Export] public static void Qux (int a, int b, int c) {} + [Export] public static void X (int x, int y) {} + [Export] public static void X (string x, int y) {} + [Export] public static void X (int x, string y) {} + [Export] public static void Bob (int x, int y, string z) {} + [Export] public static void Bob (int x, string y, int q) {} + [Export] public static void Change (double progress) {} + [Export] public static void Change (string info) {} + [Export] public static void Change (double progress, string info) {} + [Export] public static void Start (string title) {} + [Export] public static void Start (string title, string info) {} + [Export] public static void Start (string title, double progress) {} + [Export] public static void Start (string title, string info, double progress) {} + } + """)); + Execute(); + Contains( + """ + export const Class = { + foo: (a) => exports.Class_Foo(a), + fooWithA: (a) => exports.Class_Foo(a), + fooWithB: (b, a) => exports.Class_Foo(b, a), + bar: (x) => exports.Class_Bar(x), + barWithName: (x, name) => exports.Class_Bar(x, name), + baz: (x, y) => exports.Class_Baz(x, y), + bazWithXAndY: (x, y) => exports.Class_Baz(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), + x: (x, y) => exports.Class_X(x, y), + xWithStringAndInt32: (x, y) => exports.Class_X(x, y), + xWithInt32AndString: (x, y) => exports.Class_X(x, y), + bob: (x, y, z) => exports.Class_Bob(x, y, z), + bobWithQ: (x, y, q) => exports.Class_Bob(x, y, q), + change: (progress) => exports.Class_Change(progress), + changeWithInfo: (info) => exports.Class_Change(info), + changeWithProgressAndInfo: (progress, info) => exports.Class_Change(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) + }; + """); + } + [Fact] public void RespectsPrefsInStatics () { diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs index b1986be1..654d3e22 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs @@ -27,6 +27,12 @@ public static string ToFirstLower (string value) return char.ToLowerInvariant(value[0]) + value[1..]; } + public static string ToFirstUpper (string value) + { + if (value.Length == 1) return value.ToUpperInvariant(); + return char.ToUpperInvariant(value[0]) + value[1..]; + } + public static string Slugify (string value) { var bld = new StringBuilder(value.Length + 4); diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs b/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs new file mode 100644 index 00000000..75a97e70 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs @@ -0,0 +1,56 @@ +namespace Bootsharp.Publish; + +internal static class OverloadDisambiguator +{ + public static void Disambiguate (IEnumerable surfaces) + { + foreach (var surface in surfaces) + foreach (var overloaded in surface.Members.OfType() + .GroupBy(m => m.JSName).Where(g => g.Count() > 1)) + Disambiguate(surface, overloaded.ToArray()); + } + + private static void Disambiguate (SurfaceMeta srf, IReadOnlyList overloaded) + { + var argsByOverload = MapArgs(overloaded); // attempt to disambiguate only with extra arg names + foreach (var method in FindAmbiguous(argsByOverload)) // if ambiguous, try with all arg names + argsByOverload[method] = GetArgNames(method); + foreach (var method in FindAmbiguous(argsByOverload)) // if still ambiguous, use arg type names + argsByOverload[method] = GetArgTypes(method); + foreach (var (method, args) in argsByOverload) + Rename(srf, method, string.Join("And", args)); + } + + private static Dictionary MapArgs (IReadOnlyList overloaded) + { + var baseline = ResolveBaseline(overloaded); // baseline is the method that won't be renamed + var baselineArgs = baseline.Args.Select(a => a.Name).ToHashSet(); + return overloaded.Where(m => m != baseline).ToDictionary(m => m, m => m.Args + .Where(a => !baselineArgs.Contains(a.Name)) + .Select(a => ToFirstUpper(a.Name)) + // if an overload has extra args — use their names as discriminator + .ToArray() is { Length: > 0 } extra ? extra : GetArgNames(m)); + } + + private static IEnumerable FindAmbiguous (Dictionary args) => args + .GroupBy(kv => string.Join("|", kv.Value)) + .Where(g => g.Count() > 1) + .SelectMany(g => g.Select(kv => kv.Key)); + + private static string[] GetArgNames (MethodMeta method) => method.Args + .Select(a => ToFirstUpper(a.Name)).ToArray(); + + private static string[] GetArgTypes (MethodMeta method) => method.Args + .Select(a => a.Value.Type.Clr.Name).ToArray(); + + private static MethodMeta ResolveBaseline (IReadOnlyList overloaded) => overloaded + .OrderBy(m => m.Args.Count) + .ThenBy(m => string.Join("|", m.Args.Select(a => a.Value.Type.Clr.FullName))) + .First(); + + private static void Rename (SurfaceMeta srf, MethodMeta method, string discriminator) + { + var members = (IList)srf.Members; + members[members.IndexOf(method)] = method with { JSName = $"{method.JSName}With{discriminator}" }; + } +} diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs index e5df7909..038632f4 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs @@ -31,6 +31,7 @@ public void Inspect (Assembly assembly) public IReadOnlyCollection Collect () { + OverloadDisambiguator.Disambiguate([..surfaces, ..its.Values]); TypeMeta[] specialized = [..surfaces, ..its.Values, ..srd.Collect()]; var clrs = specialized.Select(t => t.Clr).ToHashSet(); return [..specialized, ..crawled.Values.Where(c => !clrs.Contains(c.Clr))]; diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 8790bb7b..6d6ef416 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.309 + 0.8.0-alpha.324 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com From 35715d2b64953e6557995470f65342bdb4bb19a8 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 22 May 2026 00:02:20 +0300 Subject: [PATCH 2/2] update docs --- docs/guide/declarations.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/guide/declarations.md b/docs/guide/declarations.md index 209aa8a4..6ca7372f 100644 --- a/docs/guide/declarations.md +++ b/docs/guide/declarations.md @@ -60,6 +60,35 @@ Bar.baz = () => {}; ::: +## Overloaded Methods + +JavaScript does not have function overloads, so Bootsharp automatically disambiguates them when projecting overloaded C# methods. The overload with the fewest parameters keeps the original name; the rest are suffixed with `With...` derived from the extra parameter names (or, when that is still ambiguous, from the full parameter names or parameter types). + +::: code-group + +```csharp [Bar.cs] +namespace Foo; + +public class Bar +{ + [Export] public static void Start (string title) {} + [Export] public static void Start (string title, string info) {} + [Export] public static void Start (string title, double progress) {} + [Export] public static void Start (string title, string info, double progress) {} +} +``` + +```ts [foo.g.d.mts] +export namespace Bar { + export function start(title: string): void; + export function startWithInfo(title: string, info: string): void; + export function startWithProgress(title: string, progress: number): void; + export function startWithInfoAndProgress(title: string, info: string, progress: number): void; +} +``` + +::: + ## Property Declarations Exported properties are emitted as variables under the declaring class's TS namespace: