Skip to content

Commit ce034ff

Browse files
committed
BridgeJS: Tests for generic imports
1 parent 0d96b11 commit ce034ff

12 files changed

Lines changed: 2230 additions & 1 deletion

File tree

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,18 @@ import Testing
2525
swiftParts.append(closureSupport)
2626
}
2727
if let exported = skeleton.exported {
28+
let hasGenericImports =
29+
skeleton.imported?.children.contains { file in
30+
file.functions.contains { !$0.genericParameters.isEmpty }
31+
} ?? false
32+
let hasGenericExports =
33+
exported.functions.contains { !$0.genericParameters.isEmpty }
34+
let hasGenerics = hasGenericImports || hasGenericExports
2835
let exportSwift = ExportSwift(
2936
progress: .silent,
3037
moduleName: skeleton.moduleName,
31-
skeleton: exported
38+
skeleton: exported,
39+
hasGenerics: hasGenerics
3240
)
3341
if let s = try exportSwift.finalize() {
3442
swiftParts.append(s)

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,37 @@ 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+
129160
@Test
130161
func perClassIdentityModeFromAnnotation() throws {
131162
let url = Self.inputsDirectory.appendingPathComponent("IdentityModeClass.swift")
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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+
// MARK: - Diagnostics
11+
12+
@Test
13+
func genericParameterRequiresBridgeableConstraint() throws {
14+
do {
15+
_ = try resolveApp(
16+
source: """
17+
@JSFunction func identity<T>(_ value: T) throws(JSException) -> T
18+
"""
19+
)
20+
Issue.record("Expected a generic-constraint diagnostic, but resolution succeeded")
21+
} catch let error as BridgeJSCoreDiagnosticError {
22+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
23+
#expect(
24+
combined.contains("Generic parameter 'T' must be constrained to '_BridgedSwiftGenericBridgeable'")
25+
)
26+
}
27+
}
28+
29+
@Test
30+
func genericWhereClauseUnsupported() throws {
31+
do {
32+
_ = try resolveApp(
33+
source: """
34+
@JSFunction func identity<T: _BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T where T: Sendable
35+
"""
36+
)
37+
Issue.record("Expected a where-clause diagnostic, but resolution succeeded")
38+
} catch let error as BridgeJSCoreDiagnosticError {
39+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
40+
#expect(combined.contains("'where' clauses are not supported on @JSFunction"))
41+
}
42+
}
43+
44+
@Test
45+
func asyncGenericImportUnsupported() throws {
46+
do {
47+
_ = try resolveApp(
48+
source: """
49+
@JSFunction func identityAsync<T: _BridgedSwiftGenericBridgeable>(_ value: T) async throws(JSException) -> T
50+
"""
51+
)
52+
Issue.record("Expected an async-generic diagnostic, but resolution succeeded")
53+
} catch let error as BridgeJSCoreDiagnosticError {
54+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
55+
#expect(combined.contains("Generic @JSFunction declarations cannot be 'async' yet."))
56+
}
57+
}
58+
59+
@Test
60+
func genericImportInsideJSClassUnsupported() throws {
61+
do {
62+
_ = try resolveApp(
63+
source: """
64+
@JSClass struct Box {
65+
@JSFunction func member<T: _BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
66+
}
67+
"""
68+
)
69+
Issue.record("Expected a @JSClass generic diagnostic, but resolution succeeded")
70+
} catch let error as BridgeJSCoreDiagnosticError {
71+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
72+
#expect(
73+
combined.contains(
74+
"Generic @JSFunction declarations are only supported at the top level, not in @JSClass types."
75+
)
76+
)
77+
}
78+
}
79+
80+
@Test
81+
func wrappedGenericOptionalReturnUnsupported() throws {
82+
do {
83+
_ = try resolveApp(
84+
source: """
85+
@JSFunction func optionalReturn<T: _BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T?
86+
"""
87+
)
88+
Issue.record("Expected a wrapped-generic diagnostic, but resolution succeeded")
89+
} catch let error as BridgeJSCoreDiagnosticError {
90+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
91+
#expect(
92+
combined.contains(
93+
"Generic parameter 'T' may only be used as a bare type; wrapping it (e.g. 'T?', '[T]') is not supported."
94+
)
95+
)
96+
}
97+
}
98+
99+
@Test
100+
func wrappedGenericArrayReturnUnsupported() throws {
101+
do {
102+
_ = try resolveApp(
103+
source: """
104+
@JSFunction func arrayReturn<T: _BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> [T]
105+
"""
106+
)
107+
Issue.record("Expected a wrapped-generic diagnostic, but resolution succeeded")
108+
} catch let error as BridgeJSCoreDiagnosticError {
109+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
110+
#expect(
111+
combined.contains(
112+
"Generic parameter 'T' may only be used as a bare type; wrapping it (e.g. 'T?', '[T]') is not supported."
113+
)
114+
)
115+
}
116+
}
117+
118+
@Test
119+
func wrappedGenericArrayParameterUnsupported() throws {
120+
do {
121+
_ = try resolveApp(
122+
source: """
123+
@JSFunction func arrayParameter<T: _BridgedSwiftGenericBridgeable>(_ values: [T]) throws(JSException) -> T
124+
"""
125+
)
126+
Issue.record("Expected a wrapped-generic diagnostic, but resolution succeeded")
127+
} catch let error as BridgeJSCoreDiagnosticError {
128+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
129+
#expect(
130+
combined.contains(
131+
"Generic parameter 'T' may only be used as a bare type; wrapping it (e.g. 'T?', '[T]') is not supported."
132+
)
133+
)
134+
}
135+
}
136+
137+
// MARK: - Positive control
138+
139+
@Test
140+
func validGenericImportParsesIntoSkeleton() throws {
141+
let skeleton = try resolveApp(
142+
source: """
143+
@JSFunction func identity<T: _BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
144+
"""
145+
)
146+
let imported = try #require(skeleton.imported)
147+
let function = try #require(
148+
imported.children.flatMap(\.functions).first(where: { $0.name == "identity" })
149+
)
150+
#expect(function.genericParameters == ["T"])
151+
#expect(function.returnType == .generic("T"))
152+
let parameter = try #require(function.parameters.first)
153+
#expect(parameter.type == .generic("T"))
154+
}
155+
156+
@Test
157+
func multiUseGenericImportParsesIntoSkeleton() throws {
158+
let skeleton = try resolveApp(
159+
source: """
160+
@JSFunction func importGenericPair<T: _BridgedSwiftGenericBridgeable>(_ a: T, _ b: T) throws(JSException) -> T
161+
"""
162+
)
163+
let imported = try #require(skeleton.imported)
164+
let function = try #require(
165+
imported.children.flatMap(\.functions).first(where: { $0.name == "importGenericPair" })
166+
)
167+
#expect(function.genericParameters == ["T"])
168+
#expect(function.returnType == .generic("T"))
169+
#expect(function.parameters.map(\.type) == [.generic("T"), .generic("T")])
170+
}
171+
172+
@Test
173+
func returnOnlyGenericImportParsesIntoSkeleton() throws {
174+
let skeleton = try resolveApp(
175+
source: """
176+
@JSFunction func importGenericMake<T: _BridgedSwiftGenericBridgeable>() throws(JSException) -> T
177+
"""
178+
)
179+
let imported = try #require(skeleton.imported)
180+
let function = try #require(
181+
imported.children.flatMap(\.functions).first(where: { $0.name == "importGenericMake" })
182+
)
183+
#expect(function.genericParameters == ["T"])
184+
#expect(function.returnType == .generic("T"))
185+
#expect(function.parameters.isEmpty)
186+
}
187+
188+
@Test
189+
func multiParameterGenericImportParsesIntoSkeleton() throws {
190+
let skeleton = try resolveApp(
191+
source: """
192+
@JSFunction func importGenericCombine<T: _BridgedSwiftGenericBridgeable, U: _BridgedSwiftGenericBridgeable>(_ a: T, _ b: U) throws(JSException) -> U
193+
"""
194+
)
195+
let imported = try #require(skeleton.imported)
196+
let function = try #require(
197+
imported.children.flatMap(\.functions).first(where: { $0.name == "importGenericCombine" })
198+
)
199+
#expect(function.genericParameters == ["T", "U"])
200+
#expect(function.returnType == .generic("U"))
201+
#expect(function.parameters.map(\.type) == [.generic("T"), .generic("U")])
202+
}
203+
204+
@Test
205+
func genericImportAcceptsHeapObjectClassAsArgument() throws {
206+
let skeleton = try resolveApp(
207+
source: """
208+
@JS final class GenericDiagImportBox {
209+
var value: Int
210+
init(value: Int) { self.value = value }
211+
func get() -> Int { value }
212+
}
213+
@JSFunction func identity<T: _BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
214+
"""
215+
)
216+
let exported = try #require(skeleton.exported)
217+
let klass = try #require(exported.classes.first(where: { $0.name == "GenericDiagImportBox" }))
218+
#expect(klass.isFinal == true)
219+
let imported = try #require(skeleton.imported)
220+
let function = try #require(
221+
imported.children.flatMap(\.functions).first(where: { $0.name == "identity" })
222+
)
223+
#expect(function.genericParameters == ["T"])
224+
#expect(function.returnType == .generic("T"))
225+
}
226+
227+
// MARK: - Utilities
228+
229+
private func resolveApp(source appSource: String) throws -> BridgeJSSkeleton {
230+
let swiftAPI = SwiftToSkeleton(
231+
progress: .silent,
232+
moduleName: "App",
233+
exposeToGlobal: false,
234+
externalModuleIndex: ExternalModuleIndex(dependencies: [])
235+
)
236+
let sourceFile = Parser.parse(source: appSource)
237+
swiftAPI.addSourceFile(sourceFile, inputFilePath: "App.swift")
238+
return try swiftAPI.finalize()
239+
}
240+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
@JS
2+
struct GenericPoint {
3+
var x: Int
4+
var y: Int
5+
}
6+
7+
@JS final class GenericImportBox {
8+
@JS var value: Int
9+
@JS init(value: Int) {
10+
self.value = value
11+
}
12+
@JS func get() -> Int {
13+
value
14+
}
15+
}
16+
17+
@JSFunction func genericRoundTrip<T: _BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
18+
19+
@JSFunction func importGenericIdentityClass<T: _BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
20+
21+
@JSFunction func genericParse<T: _BridgedSwiftGenericBridgeable>(_ json: String) throws(JSException) -> T
22+
23+
@JSFunction func importGenericPair<T: _BridgedSwiftGenericBridgeable>(_ a: T, _ b: T) throws(JSException) -> T
24+
25+
@JSFunction func importGenericMake<T: _BridgedSwiftGenericBridgeable>() throws(JSException) -> T
26+
27+
@JSFunction func importGenericCombine<T: _BridgedSwiftGenericBridgeable, U: _BridgedSwiftGenericBridgeable>(
28+
_ a: T,
29+
_ b: U
30+
) throws(JSException) -> U

0 commit comments

Comments
 (0)