Skip to content

Commit 3b74e6a

Browse files
committed
BridgeJS: Tests and fixtures for generic functions
1 parent 0664483 commit 3b74e6a

24 files changed

Lines changed: 9892 additions & 267 deletions

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import Testing
2828
let exportSwift = ExportSwift(
2929
progress: .silent,
3030
moduleName: skeleton.moduleName,
31-
skeleton: exported
31+
skeleton: exported,
32+
imported: skeleton.imported
3233
)
3334
if let s = try exportSwift.finalize() {
3435
swiftParts.append(s)

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,74 @@ 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 genericResolverIsGatedToGenericModules() throws {
151+
let genericJS = try linkedJS(forFixture: "GenericImports.swift")
152+
#expect(genericJS.contains("swift_js_resolve_type_id"))
153+
#expect(genericJS.contains("__bjs_codecs"))
154+
155+
let nonGenericJS = try linkedJS(forFixture: "SwiftStructImports.swift")
156+
#expect(!nonGenericJS.contains("swift_js_resolve_type_id"))
157+
#expect(!nonGenericJS.contains("__bjs_codecs"))
158+
}
159+
160+
@Test
161+
func duplicateGenericTokenAcrossModulesFailsLinking() throws {
162+
func makeSkeleton(moduleName: String, source: String) throws -> BridgeJSSkeleton {
163+
let swiftAPI = SwiftToSkeleton(
164+
progress: .silent,
165+
moduleName: moduleName,
166+
exposeToGlobal: false,
167+
externalModuleIndex: .empty
168+
)
169+
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "\(moduleName).swift")
170+
return try swiftAPI.finalize()
171+
}
172+
let structSource = """
173+
@JS public struct Point {
174+
public var x: Int
175+
@JS public init(x: Int) { self.x = x }
176+
}
177+
"""
178+
let first = try makeSkeleton(
179+
moduleName: "FirstModule",
180+
source: structSource + """
181+
182+
@JS public func identity<T: _BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
183+
"""
184+
)
185+
let second = try makeSkeleton(moduleName: "SecondModule", source: structSource)
186+
let bridgeJSLink = BridgeJSLink(skeletons: [first, second], sharedMemory: false)
187+
#expect {
188+
_ = try bridgeJSLink.link()
189+
} throws: { error in
190+
guard let linkError = error as? BridgeJSLinkError else { return false }
191+
return linkError.message.contains("Generic bridge token 'Point'")
192+
&& linkError.message.contains("FirstModule")
193+
&& linkError.message.contains("SecondModule")
194+
}
195+
}
196+
129197
@Test
130198
func perClassIdentityModeFromAnnotation() throws {
131199
let url = Self.inputsDirectory.appendingPathComponent("IdentityModeClass.swift")
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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 CrossModuleGenericExportTests {
10+
@Test
11+
func registersDependencyStructAndConformsRetroactivelyWhenDepLacksGenerics() throws {
12+
let core = try buildSkeleton(
13+
moduleName: "Core",
14+
source: """
15+
@JS public struct Vector3D {
16+
public let x: Double
17+
@JS public init(x: Double) { self.x = x }
18+
}
19+
"""
20+
)
21+
let dependencyStructs = DependencyGenericStruct.collect(
22+
from: [(moduleName: "Core", skeleton: core)]
23+
)
24+
#expect(dependencyStructs.count == 1)
25+
#expect(dependencyStructs.first?.definingModuleHasGenerics == false)
26+
27+
let output = try renderExportGlue(
28+
source: """
29+
import Core
30+
@JS public func use<T: _BridgedSwiftGenericBridgeable>(_ value: T) -> T {
31+
return value
32+
}
33+
""",
34+
dependencies: [(moduleName: "Core", skeleton: core)],
35+
dependencyStructs: dependencyStructs
36+
)
37+
38+
#expect(output.contains("_bjs_registerGenericExportType(Core.Vector3D.self)"))
39+
#expect(output.contains("extension Core.Vector3D: @retroactive _BridgedSwiftGenericBridgeable {"))
40+
#expect(
41+
output.contains(
42+
"@_spi(BridgeJS) public static var bridgeJSTypeName: StaticString { \"Vector3D\" }"
43+
)
44+
)
45+
#expect(
46+
output.contains(
47+
"@_spi(BridgeJS) public static let bridgeJSTypeID: Int32 = _swift_js_resolve_type_id(Core.Vector3D.bridgeJSTypeName)"
48+
)
49+
)
50+
}
51+
52+
@Test
53+
func registersDependencyStructWithoutConformanceWhenDepHasGenerics() throws {
54+
let core = try buildSkeleton(
55+
moduleName: "Core",
56+
source: """
57+
@JS public struct Vector3D {
58+
public let x: Double
59+
@JS public init(x: Double) { self.x = x }
60+
}
61+
@JS public func coreUse<T: _BridgedSwiftGenericBridgeable>(_ value: T) -> T {
62+
return value
63+
}
64+
"""
65+
)
66+
let dependencyStructs = DependencyGenericStruct.collect(
67+
from: [(moduleName: "Core", skeleton: core)]
68+
)
69+
#expect(dependencyStructs.first?.definingModuleHasGenerics == true)
70+
71+
let output = try renderExportGlue(
72+
source: """
73+
import Core
74+
@JS public func use<T: _BridgedSwiftGenericBridgeable>(_ value: T) -> T {
75+
return value
76+
}
77+
""",
78+
dependencies: [(moduleName: "Core", skeleton: core)],
79+
dependencyStructs: dependencyStructs
80+
)
81+
82+
#expect(output.contains("_bjs_registerGenericExportType(Core.Vector3D.self)"))
83+
#expect(!output.contains("@retroactive _BridgedSwiftGenericBridgeable"))
84+
}
85+
86+
@Test
87+
func registersDependencyConformanceForImportOnlyModules() throws {
88+
let core = try buildSkeleton(
89+
moduleName: "Core",
90+
source: """
91+
@JS public struct Vector3D {
92+
public let x: Double
93+
@JS public init(x: Double) { self.x = x }
94+
}
95+
"""
96+
)
97+
let dependencyStructs = DependencyGenericStruct.collect(
98+
from: [(moduleName: "Core", skeleton: core)]
99+
)
100+
101+
let app = try buildSkeleton(
102+
moduleName: "App",
103+
source: """
104+
@JSFunction func parse<T: _BridgedSwiftGenericBridgeable>(_ json: String) throws(JSException) -> T
105+
""",
106+
dependencies: [(moduleName: "Core", skeleton: core)]
107+
)
108+
#expect(app.exported == nil)
109+
110+
// Mirrors BridgeJSTool: a module with only generic imports still renders the
111+
// dependency conformances through an empty exported skeleton.
112+
let exportSwift = ExportSwift(
113+
progress: .silent,
114+
moduleName: app.moduleName,
115+
skeleton: ExportedSkeleton(functions: [], classes: [], enums: [], exposeToGlobal: false),
116+
imported: app.imported,
117+
dependencyStructs: dependencyStructs
118+
)
119+
let output = try #require(try exportSwift.finalize())
120+
121+
#expect(output.contains("extension Core.Vector3D: @retroactive _BridgedSwiftGenericBridgeable {"))
122+
// The export type registry is only consulted by export thunks.
123+
#expect(!output.contains("_bridgeJSExportTypeRegistry"))
124+
}
125+
126+
private func renderExportGlue(
127+
source: String,
128+
dependencies: [(moduleName: String, skeleton: BridgeJSSkeleton)],
129+
dependencyStructs: [DependencyGenericStruct]
130+
) throws -> String {
131+
let swiftAPI = SwiftToSkeleton(
132+
progress: .silent,
133+
moduleName: "App",
134+
exposeToGlobal: false,
135+
externalModuleIndex: ExternalModuleIndex(dependencies: dependencies)
136+
)
137+
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "App.swift")
138+
let skeleton = try swiftAPI.finalize()
139+
let exported = try #require(skeleton.exported)
140+
let exportSwift = ExportSwift(
141+
progress: .silent,
142+
moduleName: skeleton.moduleName,
143+
skeleton: exported,
144+
imported: skeleton.imported,
145+
dependencyStructs: dependencyStructs
146+
)
147+
return try #require(try exportSwift.finalize())
148+
}
149+
150+
private func buildSkeleton(
151+
moduleName: String,
152+
source: String,
153+
dependencies: [(moduleName: String, skeleton: BridgeJSSkeleton)] = []
154+
) throws -> BridgeJSSkeleton {
155+
let swiftAPI = SwiftToSkeleton(
156+
progress: .silent,
157+
moduleName: moduleName,
158+
exposeToGlobal: false,
159+
externalModuleIndex: ExternalModuleIndex(dependencies: dependencies)
160+
)
161+
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "\(moduleName).swift")
162+
return try swiftAPI.finalize()
163+
}
164+
}

0 commit comments

Comments
 (0)