Skip to content

Commit 60403ee

Browse files
committed
BridgeJS: Tests and fixtures for generic imports
1 parent f55d763 commit 60403ee

137 files changed

Lines changed: 5616 additions & 31 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ import Testing
4040
swiftParts.append(s)
4141
}
4242
}
43+
if let typeRegistration = GenericTypeRegistrationCodegen().render(for: skeleton) {
44+
swiftParts.append(typeRegistration)
45+
}
4346
let combinedSwift =
4447
swiftParts
4548
.map { $0.trimmingCharacters(in: .newlines) }

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,86 @@ import Testing
126126
try snapshot(bridgeJSLink: bridgeJSLink, name: "MixedModules")
127127
}
128128

129+
private func linkedJS(forFixture input: String) throws -> String {
130+
let url = Self.inputsDirectory.appendingPathComponent(input)
131+
let name = url.deletingPathExtension().lastPathComponent
132+
let sourceFile = Parser.parse(source: try String(contentsOf: url, encoding: .utf8))
133+
let importSwift = SwiftToSkeleton(
134+
progress: .silent,
135+
moduleName: "TestModule",
136+
exposeToGlobal: false,
137+
externalModuleIndex: .empty
138+
)
139+
importSwift.addSourceFile(sourceFile, inputFilePath: "\(name).swift")
140+
let importResult = try importSwift.finalize()
141+
var bridgeJSLink = BridgeJSLink(sharedMemory: false)
142+
let encoder = JSONEncoder()
143+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
144+
let unifiedData = try encoder.encode(importResult)
145+
try bridgeJSLink.addSkeletonFile(data: unifiedData)
146+
return try bridgeJSLink.link().0
147+
}
148+
149+
@Test
150+
func genericRuntimeIsGatedToGenericBuilds() throws {
151+
let genericJS = try linkedJS(forFixture: "GenericImports.swift")
152+
#expect(genericJS.contains("__bjs_codecByTypeId"))
153+
#expect(genericJS.contains("__bjs_primitiveCodecs"))
154+
#expect(genericJS.contains("bjs[\"bjs_TestModule_register_type_handles\"] = function(base, count) {"))
155+
#expect(genericJS.contains("instance.exports[\"bjs_TestModule_register_type_handles\"]();"))
156+
// Eager registration hook: called by the instantiate.js template right
157+
// after WASI initialization (the lazy guard remains as a fallback).
158+
#expect(genericJS.contains("afterInitialize: () => {"))
159+
160+
// Modules with @JS types but no generic declarations still emit a Swift
161+
// registration export (their types may be used by a dependent module's
162+
// generic function), so the link layer must install a no-op hook for the
163+
// wasm import — but the generic runtime itself must be omitted.
164+
let nonGenericJS = try linkedJS(forFixture: "SwiftStructImports.swift")
165+
#expect(!nonGenericJS.contains("__bjs_codecByTypeId"))
166+
#expect(!nonGenericJS.contains("__bjs_primitiveCodecs"))
167+
#expect(nonGenericJS.contains("bjs[\"bjs_TestModule_register_type_handles\"] = function() {};"))
168+
#expect(!nonGenericJS.contains("instance.exports[\"bjs_TestModule_register_type_handles\"]();"))
169+
// Without the generic runtime the hook is not emitted at all; the
170+
// instantiate template calls it with optional chaining.
171+
#expect(!nonGenericJS.contains("afterInitialize: () => {"))
172+
}
173+
174+
@Test
175+
func sameTypeNameAcrossModulesLinksWithHandleIdentity() throws {
176+
// Type identity is pointer-based (each type owns a BridgeJSTypeHandle),
177+
// so two modules defining a same-named @JS type must link fine: each
178+
// module registers its own handle IDs against its own codec array.
179+
func makeSkeleton(moduleName: String, source: String) throws -> BridgeJSSkeleton {
180+
let swiftAPI = SwiftToSkeleton(
181+
progress: .silent,
182+
moduleName: moduleName,
183+
exposeToGlobal: false,
184+
externalModuleIndex: .empty
185+
)
186+
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "\(moduleName).swift")
187+
return try swiftAPI.finalize()
188+
}
189+
let structSource = """
190+
@JS public struct Point {
191+
public var x: Int
192+
@JS public init(x: Int) { self.x = x }
193+
}
194+
"""
195+
let first = try makeSkeleton(
196+
moduleName: "FirstModule",
197+
source: structSource + """
198+
199+
@JSFunction func identity<T: BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
200+
"""
201+
)
202+
let second = try makeSkeleton(moduleName: "SecondModule", source: structSource)
203+
let bridgeJSLink = BridgeJSLink(skeletons: [first, second], sharedMemory: false)
204+
let js = try bridgeJSLink.link().outputJs
205+
#expect(js.contains("bjs[\"bjs_FirstModule_register_type_handles\"] = function(base, count) {"))
206+
#expect(js.contains("bjs[\"bjs_SecondModule_register_type_handles\"] = function(base, count) {"))
207+
}
208+
129209
@Test
130210
func perClassIdentityModeFromAnnotation() throws {
131211
let url = Self.inputsDirectory.appendingPathComponent("IdentityModeClass.swift")
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import Foundation
2+
import SwiftParser
3+
import SwiftSyntax
4+
import Testing
5+
6+
@testable import BridgeJSLink
7+
@testable import BridgeJSCore
8+
@testable import BridgeJSSkeleton
9+
10+
func makeSkeleton(
11+
_ source: String,
12+
moduleName: String = "TestModule",
13+
dependencies: [(moduleName: String, skeleton: BridgeJSSkeleton)] = []
14+
) throws -> BridgeJSSkeleton {
15+
let swiftAPI = SwiftToSkeleton(
16+
progress: .silent,
17+
moduleName: moduleName,
18+
exposeToGlobal: false,
19+
externalModuleIndex: ExternalModuleIndex(dependencies: dependencies)
20+
)
21+
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "\(moduleName).swift")
22+
return try swiftAPI.finalize()
23+
}
24+
25+
func expectDiagnostic(
26+
source: String,
27+
moduleName: String = "App",
28+
contains message: String,
29+
sourceLocation: Testing.SourceLocation = #_sourceLocation
30+
) {
31+
do {
32+
_ = try makeSkeleton(source, moduleName: moduleName)
33+
Issue.record("Expected diagnostic but resolution succeeded", sourceLocation: sourceLocation)
34+
} catch let error as BridgeJSCoreDiagnosticError {
35+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
36+
#expect(combined.contains(message), sourceLocation: sourceLocation)
37+
} catch {
38+
Issue.record("Unexpected error: \(error)", sourceLocation: sourceLocation)
39+
}
40+
}
41+
42+
func linkSource(_ source: String, moduleName: String = "TestModule") throws -> (js: String, dts: String) {
43+
let skeleton = try makeSkeleton(source, moduleName: moduleName)
44+
var bridgeJSLink = BridgeJSLink(sharedMemory: false)
45+
let encoder = JSONEncoder()
46+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
47+
let unifiedData = try encoder.encode(skeleton)
48+
try bridgeJSLink.addSkeletonFile(data: unifiedData)
49+
let result = try bridgeJSLink.link()
50+
return (result.outputJs, result.outputDts)
51+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import Testing
2+
3+
@testable import BridgeJSCore
4+
5+
@Suite struct GenericExportDiagnosticsTests {
6+
7+
@Test
8+
func genericExportedFunctionRejected() {
9+
expectDiagnostic(
10+
source: """
11+
@JS public func identity<T: BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
12+
""",
13+
contains: "Generic parameters on exported @JS functions are not supported yet"
14+
)
15+
}
16+
17+
@Test
18+
func genericMethodOnExportedClassRejected() {
19+
expectDiagnostic(
20+
source: """
21+
@JS final class Box {
22+
@JS init() {}
23+
@JS func wrap<T: BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
24+
}
25+
""",
26+
contains: "Generic parameters on exported @JS functions are not supported yet"
27+
)
28+
}
29+
30+
@Test
31+
func genericMethodOnExportedStructRejected() {
32+
expectDiagnostic(
33+
source: """
34+
@JS struct Pair {
35+
@JS init() {}
36+
@JS func first<T: BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
37+
}
38+
""",
39+
contains: "Generic parameters on exported @JS functions are not supported yet"
40+
)
41+
}
42+
43+
@Test
44+
func genericStaticMethodOnExportedEnumRejected() {
45+
expectDiagnostic(
46+
source: """
47+
@JS enum Factory {
48+
case primary
49+
@JS static func one<T: BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
50+
}
51+
""",
52+
contains: "Generic parameters on exported @JS functions are not supported yet"
53+
)
54+
}
55+
56+
@Test
57+
func unconstrainedGenericExportedFunctionRejected() {
58+
expectDiagnostic(
59+
source: """
60+
@JS public func identity<T>(_ value: T) -> T { value }
61+
""",
62+
contains: "Generic parameters on exported @JS functions are not supported yet"
63+
)
64+
}
65+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import Foundation
2+
import SwiftParser
3+
import SwiftSyntax
4+
import Testing
5+
6+
@testable import BridgeJSCore
7+
@testable import BridgeJSSkeleton
8+
9+
@Suite struct GenericImportDiagnosticsTests {
10+
11+
@Test
12+
func genericParameterRequiresBridgeableConstraint() {
13+
expectDiagnostic(
14+
source: """
15+
@JSFunction func identity<T>(_ value: T) throws(JSException) -> T
16+
""",
17+
contains: "Generic parameter 'T' must be constrained to 'BridgedSwiftGenericBridgeable'"
18+
)
19+
}
20+
21+
@Test
22+
func genericWhereClauseUnsupported() {
23+
expectDiagnostic(
24+
source: """
25+
@JSFunction func identity<T: BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T where T: Sendable
26+
""",
27+
contains: "'where' clauses are not supported on @JSFunction"
28+
)
29+
}
30+
31+
@Test
32+
func asyncGenericImportUnsupported() {
33+
expectDiagnostic(
34+
source: """
35+
@JSFunction func identityAsync<T: BridgedSwiftGenericBridgeable>(_ value: T) async throws(JSException) -> T
36+
""",
37+
contains: "Generic @JSFunction declarations cannot be 'async' yet."
38+
)
39+
}
40+
41+
@Test
42+
func genericImportedMethodIsParsed() throws {
43+
let skeleton = try makeSkeleton(
44+
"""
45+
@JSClass struct Box {
46+
@JSFunction func member<T: BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
47+
}
48+
""",
49+
moduleName: "App"
50+
)
51+
let imported = try #require(skeleton.imported)
52+
let types = imported.children.flatMap { $0.types }
53+
let box = try #require(types.first { $0.name == "Box" })
54+
let method = try #require(box.methods.first { $0.name == "member" })
55+
#expect(method.genericParameters == ["T"])
56+
}
57+
58+
@Test(arguments: [
59+
("[[T]]", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: [[T]]) throws(JSException)"),
60+
("[T?]", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: [T?]) throws(JSException)"),
61+
("T??", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: T??) throws(JSException)"),
62+
("[Int: T]", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: [Int: T]) throws(JSException)"),
63+
])
64+
func unsupportedGenericWrapperFormsInParameter(label: String, source: String) {
65+
expectDiagnostic(
66+
source: source,
67+
contains: "may only be used as a bare type"
68+
)
69+
}
70+
71+
@Test(arguments: [
72+
("[[T]]", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: T) throws(JSException) -> [[T]]"),
73+
("[T?]", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: T) throws(JSException) -> [T?]"),
74+
("T??", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: T) throws(JSException) -> T??"),
75+
("[Int: T]", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: T) throws(JSException) -> [Int: T]"),
76+
])
77+
func unsupportedGenericWrapperFormsInReturn(label: String, source: String) {
78+
expectDiagnostic(
79+
source: source,
80+
contains: "may only be used as a bare type"
81+
)
82+
}
83+
84+
@Test
85+
func genericImportedMethodAsyncIsRejected() {
86+
expectDiagnostic(
87+
source: """
88+
@JSClass struct Box {
89+
@JSFunction func member<T: BridgedSwiftGenericBridgeable>(_ value: T) async throws(JSException) -> T
90+
}
91+
""",
92+
contains: "Generic @JSFunction declarations cannot be 'async' yet."
93+
)
94+
}
95+
96+
@Test
97+
func genericImportedMethodUnconstrainedParamIsRejected() {
98+
expectDiagnostic(
99+
source: """
100+
@JSClass struct Box {
101+
@JSFunction func member<T>(_ value: T) throws(JSException) -> T
102+
}
103+
""",
104+
contains:
105+
"Generic parameter 'T' must be constrained to 'BridgedSwiftGenericBridgeable' to be used with @JSFunction."
106+
)
107+
}
108+
109+
@Test
110+
func genericImportedFunctionUnusedTypeParamIsRejected() {
111+
expectDiagnostic(
112+
source: """
113+
@JSFunction func unused<T: BridgedSwiftGenericBridgeable>() throws(JSException) -> Int
114+
""",
115+
contains:
116+
"The generic parameter 'T' must be used in a parameter or return type of a generic @JSFunction declaration."
117+
)
118+
}
119+
120+
@Test
121+
func genericImportedMethodUnusedTypeParamIsRejected() {
122+
expectDiagnostic(
123+
source: """
124+
@JSClass struct Box {
125+
@JSFunction func member<T: BridgedSwiftGenericBridgeable>() throws(JSException) -> Int
126+
}
127+
""",
128+
contains:
129+
"The generic parameter 'T' must be used in a parameter or return type of a generic @JSFunction declaration."
130+
)
131+
}
132+
133+
@Test
134+
func genericImportedReturnOnlyTypeParamIsAllowed() throws {
135+
let skeleton = try makeSkeleton(
136+
"""
137+
@JSFunction func make<T: BridgedSwiftGenericBridgeable>() throws(JSException) -> T
138+
""",
139+
moduleName: "App"
140+
)
141+
let imported = try #require(skeleton.imported)
142+
let functions = imported.children.flatMap { $0.functions }
143+
let function = try #require(functions.first { $0.name == "make" })
144+
#expect(function.genericParameters == ["T"])
145+
}
146+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Testing
2+
3+
@Suite struct GenericMethodOnlyModuleCodegenTests {
4+
@Test
5+
func importMethodOnlyEmitsJSRuntimeInfrastructure() throws {
6+
// A module with generic imports but no exported @JS types still needs a
7+
// populated codec table for the primitives, so it registers them itself.
8+
let js = try linkSource(
9+
"""
10+
@JSClass struct OnlyConsumer {
11+
@JSFunction func identity<T: BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
12+
}
13+
"""
14+
).js
15+
#expect(js.contains("const __bjs_codecByTypeId = new Map();"))
16+
#expect(js.contains("function __bjs_codecForTypeId(typeId) {"))
17+
#expect(js.contains("bjs[\"bjs_TestModule_register_type_handles\"] = function(base, count) {"))
18+
#expect(js.contains("instance.exports[\"bjs_TestModule_register_type_handles\"]();"))
19+
}
20+
}

0 commit comments

Comments
 (0)