Skip to content

Commit dbcd640

Browse files
committed
BridgeJS: Tests for generic exports
1 parent c1fa92f commit dbcd640

12 files changed

Lines changed: 4656 additions & 108 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
private func renderExportGlue(
87+
source: String,
88+
dependencies: [(moduleName: String, skeleton: BridgeJSSkeleton)],
89+
dependencyStructs: [DependencyGenericStruct]
90+
) throws -> String {
91+
let swiftAPI = SwiftToSkeleton(
92+
progress: .silent,
93+
moduleName: "App",
94+
exposeToGlobal: false,
95+
externalModuleIndex: ExternalModuleIndex(dependencies: dependencies)
96+
)
97+
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "App.swift")
98+
let skeleton = try swiftAPI.finalize()
99+
let exported = try #require(skeleton.exported)
100+
let exportSwift = ExportSwift(
101+
progress: .silent,
102+
moduleName: skeleton.moduleName,
103+
skeleton: exported,
104+
imported: skeleton.imported,
105+
dependencyStructs: dependencyStructs
106+
)
107+
return try #require(try exportSwift.finalize())
108+
}
109+
110+
private func buildSkeleton(
111+
moduleName: String,
112+
source: String,
113+
dependencies: [(moduleName: String, skeleton: BridgeJSSkeleton)] = []
114+
) throws -> BridgeJSSkeleton {
115+
let swiftAPI = SwiftToSkeleton(
116+
progress: .silent,
117+
moduleName: moduleName,
118+
exposeToGlobal: false,
119+
externalModuleIndex: ExternalModuleIndex(dependencies: dependencies)
120+
)
121+
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "\(moduleName).swift")
122+
return try swiftAPI.finalize()
123+
}
124+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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 wrappedGenericOptionalReturnUnsupported() throws {
61+
do {
62+
_ = try resolveApp(
63+
source: """
64+
@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) -> T? { v }
65+
"""
66+
)
67+
Issue.record("Expected a wrapped-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("may only be used as a bare type"))
71+
}
72+
}
73+
74+
@Test
75+
func genericExportMemberUnsupported() throws {
76+
do {
77+
_ = try resolveApp(
78+
source: """
79+
@JS class Box {
80+
@JS func member<T: _BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
81+
}
82+
"""
83+
)
84+
Issue.record("Expected a generic-member diagnostic, but resolution succeeded")
85+
} catch let error as BridgeJSCoreDiagnosticError {
86+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
87+
#expect(
88+
combined.contains("Generic @JS functions are only supported as top-level functions")
89+
)
90+
}
91+
}
92+
93+
@Test
94+
func fullyUnusedGenericParameterRejected() throws {
95+
do {
96+
_ = try resolveApp(
97+
source: """
98+
@JS public func combine<T: _BridgedSwiftGenericBridgeable, U: _BridgedSwiftGenericBridgeable>(_ a: T) -> T { a }
99+
"""
100+
)
101+
Issue.record("Expected a fully-unused-generic diagnostic, but resolution succeeded")
102+
} catch let error as BridgeJSCoreDiagnosticError {
103+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
104+
#expect(combined.contains("must be used in at least one parameter"))
105+
}
106+
}
107+
108+
@Test
109+
func genericParameterMustBeUsedAsParameter() throws {
110+
do {
111+
_ = try resolveApp(
112+
source: """
113+
@JS public func f<T: _BridgedSwiftGenericBridgeable>() -> T { fatalError() }
114+
"""
115+
)
116+
Issue.record("Expected a generic-parameter-usage 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 genericConcreteReturnUnsupported() throws {
125+
do {
126+
_ = try resolveApp(
127+
source: """
128+
@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) -> String { "" }
129+
"""
130+
)
131+
Issue.record("Expected a concrete-return 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 return the generic type or Void"))
135+
}
136+
}
137+
138+
// MARK: - Positive control
139+
140+
// MARK: - Utilities
141+
142+
private func resolveApp(source appSource: String) throws -> BridgeJSSkeleton {
143+
let swiftAPI = SwiftToSkeleton(
144+
progress: .silent,
145+
moduleName: "App",
146+
exposeToGlobal: false,
147+
externalModuleIndex: ExternalModuleIndex(dependencies: [])
148+
)
149+
let sourceFile = Parser.parse(source: appSource)
150+
swiftAPI.addSourceFile(sourceFile, inputFilePath: "App.swift")
151+
return try swiftAPI.finalize()
152+
}
153+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
@JS struct ExportPoint {
2+
var x: Int
3+
var y: Int
4+
}
5+
6+
@JS final class ExportBox {
7+
@JS var value: Int
8+
@JS init(value: Int) {
9+
self.value = value
10+
}
11+
@JS func get() -> Int {
12+
value
13+
}
14+
}
15+
16+
@JS public func genericExportIdentity<T: _BridgedSwiftGenericBridgeable>(_ value: T) -> T {
17+
return value
18+
}
19+
20+
@JS public func genericExportIdentityClass<T: _BridgedSwiftGenericBridgeable>(_ value: T) -> T {
21+
return value
22+
}
23+
24+
@JS public func genericExportRoundTrip<T: _BridgedSwiftGenericBridgeable>(_ value: T) -> T {
25+
return value
26+
}
27+
28+
@JS public func genericExportEcho<T: _BridgedSwiftGenericBridgeable>(_ value: T, tag: Int) -> T {
29+
return value
30+
}
31+
32+
@JS public func genericExportLabeled<T: _BridgedSwiftGenericBridgeable>(prefix: String, _ value: T) -> T {
33+
return value
34+
}
35+
36+
@JS public func genericExportStructConcrete<T: _BridgedSwiftGenericBridgeable>(_ p: ExportPoint, _ v: T) -> T {
37+
return v
38+
}
39+
40+
@JS public func genericExportStructConcreteLeading<T: _BridgedSwiftGenericBridgeable>(_ v: T, _ p: ExportPoint) -> T {
41+
return v
42+
}
43+
44+
@JS public func genericExportTwoStructConcrete<T: _BridgedSwiftGenericBridgeable>(
45+
_ a: ExportPoint,
46+
_ b: ExportPoint,
47+
_ v: T
48+
) -> T {
49+
return v
50+
}
51+
52+
@JS public func genericExportStructAndScalar<T: _BridgedSwiftGenericBridgeable>(_ p: ExportPoint, tag: Int, _ v: T) -> T
53+
{
54+
return v
55+
}
56+
57+
@JS public func genericExportArrayConcrete<T: _BridgedSwiftGenericBridgeable>(_ xs: [Int], _ v: T) -> T {
58+
return v
59+
}
60+
61+
@JS public func genericExportPair<T: _BridgedSwiftGenericBridgeable>(_ a: T, _ b: T) -> T {
62+
return a
63+
}
64+
65+
@JS public func genericExportPairWithStruct<T: _BridgedSwiftGenericBridgeable>(_ p: ExportPoint, _ a: T, _ b: T) -> T {
66+
return b
67+
}
68+
69+
@JS public func genericExportCombine<T: _BridgedSwiftGenericBridgeable, U: _BridgedSwiftGenericBridgeable>(
70+
_ a: T,
71+
_ b: U
72+
) -> T {
73+
return a
74+
}
75+
76+
@JS public func genericExportCombineReturnU<T: _BridgedSwiftGenericBridgeable, U: _BridgedSwiftGenericBridgeable>(
77+
_ a: T,
78+
_ b: U
79+
) -> U {
80+
return b
81+
}
82+
83+
@JS
84+
public func genericExportCombineWithStruct<
85+
T: _BridgedSwiftGenericBridgeable,
86+
U: _BridgedSwiftGenericBridgeable
87+
>(_ p: ExportPoint, _ a: T, _ b: U) -> T {
88+
return a
89+
}
90+
91+
@JS
92+
public func genericExportCombineMix<
93+
T: _BridgedSwiftGenericBridgeable,
94+
U: _BridgedSwiftGenericBridgeable
95+
>(_ a: T, _ b: T, _ c: U) -> U {
96+
return c
97+
}

0 commit comments

Comments
 (0)