Skip to content

Commit 6e3da66

Browse files
committed
BridgeJS: Tests and fixtures for generic functions
1 parent 5b24b56 commit 6e3da66

25 files changed

Lines changed: 10634 additions & 526 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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 renderExportGlue(
26+
_ source: String,
27+
moduleName: String = "TestModule"
28+
) throws -> String {
29+
let skeleton = try makeSkeleton(source, moduleName: moduleName)
30+
let exported = try #require(skeleton.exported)
31+
let exportSwift = ExportSwift(
32+
progress: .silent,
33+
moduleName: skeleton.moduleName,
34+
skeleton: exported,
35+
imported: skeleton.imported
36+
)
37+
return try #require(try exportSwift.finalize())
38+
}
39+
40+
func renderImportGlue(_ source: String, moduleName: String = "TestModule") throws -> String {
41+
let skeleton = try makeSkeleton(source, moduleName: moduleName)
42+
let imported = try #require(skeleton.imported)
43+
let importTS = ImportTS(progress: .silent, moduleName: skeleton.moduleName, skeleton: imported)
44+
return try #require(try importTS.finalize())
45+
}
46+
47+
func expectDiagnostic(
48+
source: String,
49+
moduleName: String = "App",
50+
contains message: String,
51+
sourceLocation: Testing.SourceLocation = #_sourceLocation
52+
) {
53+
do {
54+
_ = try makeSkeleton(source, moduleName: moduleName)
55+
Issue.record("Expected diagnostic but resolution succeeded", sourceLocation: sourceLocation)
56+
} catch let error as BridgeJSCoreDiagnosticError {
57+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
58+
#expect(combined.contains(message), sourceLocation: sourceLocation)
59+
} catch {
60+
Issue.record("Unexpected error: \(error)", sourceLocation: sourceLocation)
61+
}
62+
}
63+
64+
func linkSource(_ source: String, moduleName: String = "TestModule") throws -> (js: String, dts: String) {
65+
let skeleton = try makeSkeleton(source, moduleName: moduleName)
66+
var bridgeJSLink = BridgeJSLink(sharedMemory: false)
67+
let encoder = JSONEncoder()
68+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
69+
let unifiedData = try encoder.encode(skeleton)
70+
try bridgeJSLink.addSkeletonFile(data: unifiedData)
71+
let result = try bridgeJSLink.link()
72+
return (result.outputJs, result.outputDts)
73+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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+
11+
@Test
12+
func genericParameterRequiresBridgeableConstraint() {
13+
expectDiagnostic(
14+
source: """
15+
@JS public func identity<T>(_ value: T) -> T { value }
16+
""",
17+
contains: "Generic parameter 'T' must be constrained to '_BridgedSwiftGenericBridgeable'"
18+
)
19+
}
20+
21+
@Test
22+
func genericWhereClauseUnsupported() {
23+
expectDiagnostic(
24+
source: """
25+
@JS public func identity<T: _BridgedSwiftGenericBridgeable>(_ value: T) -> T where T: Sendable { value }
26+
""",
27+
contains: "'where' clauses are not supported"
28+
)
29+
}
30+
31+
@Test
32+
func asyncGenericExportUnsupported() {
33+
expectDiagnostic(
34+
source: """
35+
@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) async -> T { v }
36+
""",
37+
contains: "Generic @JS functions cannot be 'async' yet."
38+
)
39+
}
40+
41+
@Test
42+
func throwsGenericExportUnsupported() {
43+
expectDiagnostic(
44+
source: """
45+
@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) throws(JSException) -> T { v }
46+
""",
47+
contains: "Generic @JS functions cannot be 'throws' yet."
48+
)
49+
}
50+
51+
@Test(arguments: [
52+
("[[T]]", "@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: [[T]]) {}"),
53+
("[T?]", "@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: [T?]) {}"),
54+
("T??", "@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T??) {}"),
55+
("[Int: T]", "@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: [Int: T]) {}"),
56+
])
57+
func unsupportedGenericWrapperFormsInParameter(label: String, source: String) {
58+
expectDiagnostic(
59+
source: source,
60+
contains: "may only be used as a bare type"
61+
)
62+
}
63+
64+
@Test(arguments: [
65+
("[[T]]", "@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) -> [[T]] { [[v]] }"),
66+
("[T?]", "@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) -> [T?] { [v] }"),
67+
("T??", "@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) -> T?? { v }"),
68+
("[Int: T]", "@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) -> [Int: T] { [0: v] }"),
69+
])
70+
func unsupportedGenericWrapperFormsInReturn(label: String, source: String) {
71+
expectDiagnostic(
72+
source: source,
73+
contains: "may only be used as a bare type"
74+
)
75+
}
76+
77+
@Test
78+
func fullyUnusedGenericParameterRejected() {
79+
expectDiagnostic(
80+
source: """
81+
@JS public func combine<T: _BridgedSwiftGenericBridgeable, U: _BridgedSwiftGenericBridgeable>(_ a: T) -> T { a }
82+
""",
83+
contains: "must be used in at least one parameter"
84+
)
85+
}
86+
87+
@Test
88+
func genericParameterMustBeUsedAsParameter() {
89+
expectDiagnostic(
90+
source: """
91+
@JS public func f<T: _BridgedSwiftGenericBridgeable>() -> T { fatalError() }
92+
""",
93+
contains: "must be used in at least one parameter"
94+
)
95+
}
96+
97+
@Test
98+
func genericConcreteReturnUnsupported() {
99+
expectDiagnostic(
100+
source: """
101+
@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) -> String { "" }
102+
""",
103+
contains: "must return the generic type"
104+
)
105+
}
106+
107+
@Test
108+
func genericInstanceMethodAsyncIsRejected() {
109+
expectDiagnostic(
110+
source: """
111+
@JS class Box {
112+
@JS init() {}
113+
@JS func wrap<T: _BridgedSwiftGenericBridgeable>(_ v: T) async -> T { v }
114+
}
115+
""",
116+
contains: "Generic @JS functions cannot be 'async' yet."
117+
)
118+
}
119+
120+
@Test
121+
func genericInstanceMethodThrowsIsRejected() {
122+
expectDiagnostic(
123+
source: """
124+
@JS class Box {
125+
@JS init() {}
126+
@JS func wrap<T: _BridgedSwiftGenericBridgeable>(_ v: T) throws(JSException) -> T { v }
127+
}
128+
""",
129+
contains: "Generic @JS functions cannot be 'throws' yet."
130+
)
131+
}
132+
133+
@Test
134+
func genericMethodWhereClauseIsRejected() {
135+
expectDiagnostic(
136+
source: """
137+
@JS class Box {
138+
@JS init() {}
139+
@JS func wrap<T: _BridgedSwiftGenericBridgeable>(_ v: T) -> T where T: Sendable { v }
140+
}
141+
""",
142+
contains: "'where' clauses are not supported on generic @JS functions."
143+
)
144+
}
145+
146+
@Test
147+
func genericMethodUnconstrainedParamIsRejected() {
148+
expectDiagnostic(
149+
source: """
150+
@JS class Box {
151+
@JS init() {}
152+
@JS func f<T>(_ v: T) -> T { v }
153+
}
154+
""",
155+
contains:
156+
"Generic parameter 'T' must be constrained to '_BridgedSwiftGenericBridgeable' to be used with @JS."
157+
)
158+
}
159+
160+
@Test
161+
func genericMethodConcreteReturnIsRejected() {
162+
expectDiagnostic(
163+
source: """
164+
@JS class Box {
165+
@JS init() {}
166+
@JS func count<T: _BridgedSwiftGenericBridgeable>(_ v: T) -> Int { 0 }
167+
}
168+
""",
169+
contains: "must return the generic type"
170+
)
171+
}
172+
173+
@Test
174+
func genericEnumInstanceMethodIsRejected() {
175+
expectDiagnostic(
176+
source: """
177+
@JS enum E {
178+
case a
179+
@JS func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) -> T { v }
180+
}
181+
""",
182+
contains: "Only static functions are supported in enums"
183+
)
184+
}
185+
186+
@Test
187+
func genericTypedPropertyIsRejectedAsUnsupportedType() {
188+
expectDiagnostic(
189+
source: """
190+
@JS final class Box {
191+
@JS init() {}
192+
@JS var value: T
193+
}
194+
""",
195+
contains: "Unsupported type 'T'."
196+
)
197+
}
198+
}

0 commit comments

Comments
 (0)