Skip to content

Commit 06bddea

Browse files
committed
BridgeJS: Reject throws generic exports and qualify nested type tokens
1 parent ef81857 commit 06bddea

9 files changed

Lines changed: 197 additions & 6 deletions

File tree

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,13 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
12991299
)
13001300
return nil
13011301
}
1302+
if node.signature.effectSpecifiers?.throwsClause != nil {
1303+
diagnose(
1304+
node: node,
1305+
message: "Generic @JS functions cannot be 'throws' yet."
1306+
)
1307+
return nil
1308+
}
13021309
}
13031310

13041311
let name = node.name.text

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -588,15 +588,25 @@ public struct BridgeJSLink {
588588
("String", BridgeType.string.tsType),
589589
("JSValue", BridgeType.jsValue.tsType),
590590
]
591+
func tsQualifiedName(name: String, namespace: [String]?) -> String {
592+
((namespace ?? []) + [name]).joined(separator: ".")
593+
}
591594
let structs = skeletons.compactMap { $0.exported?.structs }.flatMap { $0 }
592595
let classes = skeletons.compactMap { $0.exported?.classes }.flatMap { $0 }
593596
let enums = skeletons.compactMap { $0.exported?.enums }.flatMap { $0 }
594597
var tokens: [(token: String, tsType: String)] = primitives
595598
for structDef in structs {
596-
tokens.append((token: structDef.abiName, tsType: structDef.name))
599+
tokens.append(
600+
(
601+
token: structDef.abiName,
602+
tsType: tsQualifiedName(name: structDef.name, namespace: structDef.namespace)
603+
)
604+
)
597605
}
598606
for klass in classes where klass.isFinal == true {
599-
tokens.append((token: klass.abiName, tsType: klass.name))
607+
tokens.append(
608+
(token: klass.abiName, tsType: tsQualifiedName(name: klass.name, namespace: klass.namespace))
609+
)
600610
}
601611
for enumDef in enums {
602612
guard let bridgeType = genericEnumBridgeType(enumDef) else { continue }

Plugins/BridgeJS/Tests/BridgeJSToolTests/GenericExportDiagnosticsTests.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@ import Testing
5656
}
5757
}
5858

59+
@Test
60+
func throwsGenericExportUnsupported() throws {
61+
do {
62+
_ = try resolveApp(
63+
source: """
64+
@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) throws(JSException) -> T { v }
65+
"""
66+
)
67+
Issue.record("Expected a throws-generic diagnostic, but resolution succeeded")
68+
} catch let error as BridgeJSCoreDiagnosticError {
69+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
70+
#expect(combined.contains("Generic @JS functions cannot be 'throws' yet."))
71+
}
72+
}
73+
5974
@Test
6075
func nestedWrappedGenericReturnUnsupported() throws {
6176
do {

Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/GenericExports.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
var y: Int
44
}
55

6+
@JS enum ExportNamespace {
7+
@JS struct Metadata {
8+
var label: String
9+
var count: Int
10+
}
11+
}
12+
613
@JS enum ExportMode: String {
714
case on
815
case off

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/GenericExports.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,21 @@
6767
}
6868
],
6969
"enums" : [
70+
{
71+
"cases" : [
72+
73+
],
74+
"emitStyle" : "const",
75+
"name" : "ExportNamespace",
76+
"staticMethods" : [
77+
78+
],
79+
"staticProperties" : [
80+
81+
],
82+
"swiftCallName" : "ExportNamespace",
83+
"tsFullPath" : "ExportNamespace"
84+
},
7085
{
7186
"cases" : [
7287
{
@@ -942,6 +957,47 @@
942957
}
943958
],
944959
"swiftCallName" : "ExportPoint"
960+
},
961+
{
962+
"methods" : [
963+
964+
],
965+
"name" : "Metadata",
966+
"namespace" : [
967+
"ExportNamespace"
968+
],
969+
"properties" : [
970+
{
971+
"isReadonly" : true,
972+
"isStatic" : false,
973+
"name" : "label",
974+
"namespace" : [
975+
"ExportNamespace"
976+
],
977+
"type" : {
978+
"string" : {
979+
980+
}
981+
}
982+
},
983+
{
984+
"isReadonly" : true,
985+
"isStatic" : false,
986+
"name" : "count",
987+
"namespace" : [
988+
"ExportNamespace"
989+
],
990+
"type" : {
991+
"integer" : {
992+
"_0" : {
993+
"isSigned" : true,
994+
"width" : "word"
995+
}
996+
}
997+
}
998+
}
999+
],
1000+
"swiftCallName" : "ExportNamespace.Metadata"
9451001
}
9461002
]
9471003
},

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/GenericExports.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,59 @@ extension ExportPoint: _BridgedSwiftGenericBridgeable {
128128
@_spi(BridgeJS) public static let bridgeJSTypeID: Int32 = _swift_js_resolve_type_id(ExportPoint.bridgeJSTypeName)
129129
}
130130

131+
extension ExportNamespace.Metadata: _BridgedSwiftStruct {
132+
@_spi(BridgeJS) @_transparent public static func bridgeJSStackPop() -> ExportNamespace.Metadata {
133+
let count = Int.bridgeJSStackPop()
134+
let label = String.bridgeJSStackPop()
135+
return ExportNamespace.Metadata(label: label, count: count)
136+
}
137+
138+
@_spi(BridgeJS) @_transparent public consuming func bridgeJSStackPush() {
139+
self.label.bridgeJSStackPush()
140+
self.count.bridgeJSStackPush()
141+
}
142+
143+
init(unsafelyCopying jsObject: JSObject) {
144+
_bjs_struct_lower_ExportNamespace_Metadata(jsObject.bridgeJSLowerParameter())
145+
self = Self.bridgeJSStackPop()
146+
}
147+
148+
func toJSObject() -> JSObject {
149+
let __bjs_self = self
150+
__bjs_self.bridgeJSStackPush()
151+
return JSObject(id: UInt32(bitPattern: _bjs_struct_lift_ExportNamespace_Metadata()))
152+
}
153+
}
154+
155+
#if arch(wasm32)
156+
@_extern(wasm, module: "bjs", name: "swift_js_struct_lower_ExportNamespace_Metadata")
157+
fileprivate func _bjs_struct_lower_ExportNamespace_Metadata_extern(_ objectId: Int32) -> Void
158+
#else
159+
fileprivate func _bjs_struct_lower_ExportNamespace_Metadata_extern(_ objectId: Int32) -> Void {
160+
fatalError("Only available on WebAssembly")
161+
}
162+
#endif
163+
@inline(never) fileprivate func _bjs_struct_lower_ExportNamespace_Metadata(_ objectId: Int32) -> Void {
164+
return _bjs_struct_lower_ExportNamespace_Metadata_extern(objectId)
165+
}
166+
167+
#if arch(wasm32)
168+
@_extern(wasm, module: "bjs", name: "swift_js_struct_lift_ExportNamespace_Metadata")
169+
fileprivate func _bjs_struct_lift_ExportNamespace_Metadata_extern() -> Int32
170+
#else
171+
fileprivate func _bjs_struct_lift_ExportNamespace_Metadata_extern() -> Int32 {
172+
fatalError("Only available on WebAssembly")
173+
}
174+
#endif
175+
@inline(never) fileprivate func _bjs_struct_lift_ExportNamespace_Metadata() -> Int32 {
176+
return _bjs_struct_lift_ExportNamespace_Metadata_extern()
177+
}
178+
179+
extension ExportNamespace.Metadata: _BridgedSwiftGenericBridgeable {
180+
@_spi(BridgeJS) public static var bridgeJSTypeName: StaticString { "ExportNamespace_Metadata" }
181+
@_spi(BridgeJS) public static let bridgeJSTypeID: Int32 = _swift_js_resolve_type_id(ExportNamespace.Metadata.bridgeJSTypeName)
182+
}
183+
131184
#if hasFeature(Embedded)
132185
@_expose(wasm, "bjs_genericExportIdentity")
133186
@_cdecl("bjs_genericExportIdentity")
@@ -708,6 +761,7 @@ private func _bjs_ensureExportTypeRegistry() {
708761
_bjs_registerGenericExportType(String.self)
709762
_bjs_registerGenericExportType(JSValue.self)
710763
_bjs_registerGenericExportType(ExportPoint.self)
764+
_bjs_registerGenericExportType(ExportNamespace.Metadata.self)
711765
_bjs_registerGenericExportType(ExportBox.self)
712766
_bjs_registerGenericExportType(ExportMode.self)
713767
_bjs_registerGenericExportType(ExportColor.self)

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/GenericExports.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,19 @@ export interface ExportPoint {
3131
y: number;
3232
}
3333
export type BridgeType<T> = string & { readonly __bridgeType?: (value: T) => void };
34-
export const BridgeTypes: { Bool: BridgeType<boolean>; Int: BridgeType<number>; Int8: BridgeType<number>; UInt8: BridgeType<number>; Int16: BridgeType<number>; UInt16: BridgeType<number>; Int32: BridgeType<number>; UInt32: BridgeType<number>; UInt: BridgeType<number>; Int64: BridgeType<bigint>; UInt64: BridgeType<bigint>; Float: BridgeType<number>; Double: BridgeType<number>; String: BridgeType<string>; JSValue: BridgeType<any>; ExportPoint: BridgeType<ExportPoint>; ExportBox: BridgeType<ExportBox>; ExportMode: BridgeType<ExportModeTag>; ExportColor: BridgeType<ExportColorTag>; ExportTagged: BridgeType<ExportTaggedTag>; };
34+
export const BridgeTypes: { Bool: BridgeType<boolean>; Int: BridgeType<number>; Int8: BridgeType<number>; UInt8: BridgeType<number>; Int16: BridgeType<number>; UInt16: BridgeType<number>; Int32: BridgeType<number>; UInt32: BridgeType<number>; UInt: BridgeType<number>; Int64: BridgeType<bigint>; UInt64: BridgeType<bigint>; Float: BridgeType<number>; Double: BridgeType<number>; String: BridgeType<string>; JSValue: BridgeType<any>; ExportPoint: BridgeType<ExportPoint>; ExportNamespace_Metadata: BridgeType<ExportNamespace.Metadata>; ExportBox: BridgeType<ExportBox>; ExportMode: BridgeType<ExportModeTag>; ExportColor: BridgeType<ExportColorTag>; ExportTagged: BridgeType<ExportTaggedTag>; };
3535
export type ExportModeObject = typeof ExportModeValues;
3636

3737
export type ExportColorObject = typeof ExportColorValues;
3838

3939
export type ExportTaggedObject = typeof ExportTaggedValues;
4040

41+
export namespace ExportNamespace {
42+
export interface Metadata {
43+
label: string;
44+
count: number;
45+
}
46+
}
4147
/// Represents a Swift heap object like a class instance or an actor instance.
4248
export interface SwiftHeapObject {
4349
/// Release the heap object.

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/GenericExports.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const ExportTaggedValues = {
2020
Text: 1,
2121
},
2222
};
23-
export const BridgeTypes = { Bool: "Bool", Int: "Int", Int8: "Int8", UInt8: "UInt8", Int16: "Int16", UInt16: "UInt16", Int32: "Int32", UInt32: "UInt32", UInt: "UInt", Int64: "Int64", UInt64: "UInt64", Float: "Float", Double: "Double", String: "String", JSValue: "JSValue", ExportPoint: "ExportPoint", ExportBox: "ExportBox", ExportMode: "ExportMode", ExportColor: "ExportColor", ExportTagged: "ExportTagged" };
23+
export const BridgeTypes = { Bool: "Bool", Int: "Int", Int8: "Int8", UInt8: "UInt8", Int16: "Int16", UInt16: "UInt16", Int32: "Int32", UInt32: "UInt32", UInt: "UInt", Int64: "Int64", UInt64: "UInt64", Float: "Float", Double: "Double", String: "String", JSValue: "JSValue", ExportPoint: "ExportPoint", ExportNamespace_Metadata: "ExportNamespace_Metadata", ExportBox: "ExportBox", ExportMode: "ExportMode", ExportColor: "ExportColor", ExportTagged: "ExportTagged" };
2424
export async function createInstantiator(options, swift) {
2525
let instance;
2626
let memory;
@@ -178,6 +178,20 @@ export async function createInstantiator(options, swift) {
178178
return { x: int1, y: int };
179179
}
180180
});
181+
const __bjs_createExportNamespace_MetadataHelpers = () => ({
182+
lower: (value) => {
183+
const bytes = textEncoder.encode(value.label);
184+
const id = swift.memory.retain(bytes);
185+
i32Stack.push(bytes.length);
186+
i32Stack.push(id);
187+
i32Stack.push((value.count | 0));
188+
},
189+
lift: () => {
190+
const int = i32Stack.pop();
191+
const string = strStack.pop();
192+
return { label: string, count: int };
193+
}
194+
});
181195
const __bjs_createExportTaggedValuesHelpers = () => ({
182196
lower: (value) => {
183197
const enumTag = value.tag;
@@ -293,6 +307,13 @@ export async function createInstantiator(options, swift) {
293307
const value = structHelpers.ExportPoint.lift();
294308
return swift.memory.retain(value);
295309
}
310+
bjs["swift_js_struct_lower_ExportNamespace_Metadata"] = function(objectId) {
311+
structHelpers.ExportNamespace_Metadata.lower(swift.memory.getObject(objectId));
312+
}
313+
bjs["swift_js_struct_lift_ExportNamespace_Metadata"] = function() {
314+
const value = structHelpers.ExportNamespace_Metadata.lift();
315+
return swift.memory.retain(value);
316+
}
296317
__bjs_codecs = {
297318
"Bool": {
298319
lower: (v) => {
@@ -447,6 +468,15 @@ export async function createInstantiator(options, swift) {
447468
return struct;
448469
},
449470
},
471+
"ExportNamespace_Metadata": {
472+
lower: (v) => {
473+
structHelpers.ExportNamespace_Metadata.lower(v);
474+
},
475+
lift: () => {
476+
const struct = structHelpers.ExportNamespace_Metadata.lift();
477+
return struct;
478+
},
479+
},
450480
"ExportBox": {
451481
lower: (v) => {
452482
ptrStack.push(v.pointer);
@@ -696,6 +726,9 @@ export async function createInstantiator(options, swift) {
696726
const ExportPointHelpers = __bjs_createExportPointHelpers();
697727
structHelpers.ExportPoint = ExportPointHelpers;
698728

729+
const ExportNamespace_MetadataHelpers = __bjs_createExportNamespace_MetadataHelpers();
730+
structHelpers.ExportNamespace_Metadata = ExportNamespace_MetadataHelpers;
731+
699732
const ExportTaggedHelpers = __bjs_createExportTaggedValuesHelpers();
700733
enumHelpers.ExportTagged = ExportTaggedHelpers;
701734

@@ -911,6 +944,8 @@ export async function createInstantiator(options, swift) {
911944
ExportMode: ExportModeValues,
912945
ExportColor: ExportColorValues,
913946
ExportTagged: ExportTaggedValues,
947+
ExportNamespace: {
948+
},
914949
};
915950
_exports = exports;
916951
return exports;

Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Unsupported-Features.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ Generic functions are supported in both directions, through a type parameter con
4242
- `async` generic functions.
4343
- Generic methods inside `@JSClass` types or static members (generics are top-level only).
4444
- `where` clauses on a generic declaration.
45-
- A declared generic parameter that is not used in any parameter.
46-
- An exported generic function that returns a concrete, non-`Void` type. The result of an exported generic function must be one of the declared generic parameters or `Void`.
45+
- An exported `@JS` generic function that is `throws`. (Imported `@JSFunction` generics may still use `throws(JSException)`.)
46+
- A declared generic parameter on an exported `@JS` function that is not used in any parameter. A return-only generic (such as `make<T>() -> T`) is supported for imported `@JSFunction`s, where the JavaScript implementation produces the value.
47+
- An exported generic function that returns a concrete, non-`Void` type. The result of an exported generic function must be one of the declared generic parameters (optionally wrapped in `[T]`, `T?`, or `[String: T]`) or `Void`.
4748

4849
The generic parameter may be used bare (`T`) or wrapped in `[T]`, `T?`, or `[String: T]`. Nested or other wrappings, such as `[T?]`, `[[T]]`, `T??`, or `[Int: T]`, are not supported and produce build-time diagnostics. `JSObject` cannot be used as the generic argument (it is a non-final class); use `JSValue` instead.
4950

0 commit comments

Comments
 (0)