Skip to content

Commit 9a1b68a

Browse files
committed
BridgeJS: Tests and fixtures for generic functions
1 parent 3008617 commit 9a1b68a

23 files changed

Lines changed: 9728 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: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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 GenericExportDiagnosticsTests {
10+
// MARK: - Diagnostics
11+
12+
@Test
13+
func genericParameterRequiresBridgeableConstraint() throws {
14+
do {
15+
_ = try resolveApp(
16+
source: """
17+
@JS public func identity<T>(_ value: T) -> T { value }
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+
@JS public func identity<T: _BridgedSwiftGenericBridgeable>(_ value: T) -> T where T: Sendable { value }
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"))
41+
}
42+
}
43+
44+
@Test
45+
func asyncGenericExportUnsupported() throws {
46+
do {
47+
_ = try resolveApp(
48+
source: """
49+
@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) async -> T { v }
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 @JS functions cannot be 'async' yet."))
56+
}
57+
}
58+
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+
74+
@Test
75+
func nestedWrappedGenericReturnUnsupported() throws {
76+
do {
77+
_ = try resolveApp(
78+
source: """
79+
@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) -> [[T]] { [[v]] }
80+
"""
81+
)
82+
Issue.record("Expected a wrapped-generic diagnostic, but resolution succeeded")
83+
} catch let error as BridgeJSCoreDiagnosticError {
84+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
85+
#expect(combined.contains("may only be used as a bare type"))
86+
}
87+
}
88+
89+
@Test
90+
func genericExportMemberUnsupported() throws {
91+
do {
92+
_ = try resolveApp(
93+
source: """
94+
@JS class Box {
95+
@JS func member<T: _BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
96+
}
97+
"""
98+
)
99+
Issue.record("Expected a generic-member diagnostic, but resolution succeeded")
100+
} catch let error as BridgeJSCoreDiagnosticError {
101+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
102+
#expect(
103+
combined.contains("Generic @JS functions are only supported as top-level functions")
104+
)
105+
}
106+
}
107+
108+
@Test
109+
func fullyUnusedGenericParameterRejected() throws {
110+
do {
111+
_ = try resolveApp(
112+
source: """
113+
@JS public func combine<T: _BridgedSwiftGenericBridgeable, U: _BridgedSwiftGenericBridgeable>(_ a: T) -> T { a }
114+
"""
115+
)
116+
Issue.record("Expected a fully-unused-generic diagnostic, but resolution succeeded")
117+
} catch let error as BridgeJSCoreDiagnosticError {
118+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
119+
#expect(combined.contains("must be used in at least one parameter"))
120+
}
121+
}
122+
123+
@Test
124+
func genericParameterMustBeUsedAsParameter() throws {
125+
do {
126+
_ = try resolveApp(
127+
source: """
128+
@JS public func f<T: _BridgedSwiftGenericBridgeable>() -> T { fatalError() }
129+
"""
130+
)
131+
Issue.record("Expected a generic-parameter-usage diagnostic, but resolution succeeded")
132+
} catch let error as BridgeJSCoreDiagnosticError {
133+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
134+
#expect(combined.contains("must be used in at least one parameter"))
135+
}
136+
}
137+
138+
@Test
139+
func genericConcreteReturnUnsupported() throws {
140+
do {
141+
_ = try resolveApp(
142+
source: """
143+
@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) -> String { "" }
144+
"""
145+
)
146+
Issue.record("Expected a concrete-return diagnostic, but resolution succeeded")
147+
} catch let error as BridgeJSCoreDiagnosticError {
148+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
149+
#expect(combined.contains("must return the generic type"))
150+
}
151+
}
152+
153+
// MARK: - Utilities
154+
155+
private func resolveApp(source appSource: String) throws -> BridgeJSSkeleton {
156+
let swiftAPI = SwiftToSkeleton(
157+
progress: .silent,
158+
moduleName: "App",
159+
exposeToGlobal: false,
160+
externalModuleIndex: ExternalModuleIndex(dependencies: [])
161+
)
162+
let sourceFile = Parser.parse(source: appSource)
163+
swiftAPI.addSourceFile(sourceFile, inputFilePath: "App.swift")
164+
return try swiftAPI.finalize()
165+
}
166+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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 nestedArrayGenericReturnUnsupported() throws {
82+
do {
83+
_ = try resolveApp(
84+
source: """
85+
@JSFunction func nestedReturn<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 beyond 'T?', '[T]' and '[String: T]' is not supported."
94+
)
95+
)
96+
}
97+
}
98+
99+
// MARK: - Utilities
100+
101+
private func resolveApp(source appSource: String) throws -> BridgeJSSkeleton {
102+
let swiftAPI = SwiftToSkeleton(
103+
progress: .silent,
104+
moduleName: "App",
105+
exposeToGlobal: false,
106+
externalModuleIndex: ExternalModuleIndex(dependencies: [])
107+
)
108+
let sourceFile = Parser.parse(source: appSource)
109+
swiftAPI.addSourceFile(sourceFile, inputFilePath: "App.swift")
110+
return try swiftAPI.finalize()
111+
}
112+
}

0 commit comments

Comments
 (0)