Skip to content

Commit 9a6fc87

Browse files
committed
BridgeJS: Tests and fixtures for generic functions
1 parent f6194ef commit 9a6fc87

22 files changed

Lines changed: 9569 additions & 189 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: 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: 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: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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 nestedWrappedGenericReturnUnsupported() 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 optionalArrayElementGenericParameterUnsupported() throws {
76+
do {
77+
_ = try resolveApp(
78+
source: """
79+
@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ values: [T?]) -> T { values[0]! }
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 nonStringDictionaryGenericParameterUnsupported() throws {
91+
do {
92+
_ = try resolveApp(
93+
source: """
94+
@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ values: [Int: T]) -> T { values[0]! }
95+
"""
96+
)
97+
Issue.record("Expected a wrapped-generic diagnostic, but resolution succeeded")
98+
} catch let error as BridgeJSCoreDiagnosticError {
99+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
100+
#expect(combined.contains("may only be used as a bare type"))
101+
}
102+
}
103+
104+
@Test
105+
func genericExportMemberUnsupported() throws {
106+
do {
107+
_ = try resolveApp(
108+
source: """
109+
@JS class Box {
110+
@JS func member<T: _BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
111+
}
112+
"""
113+
)
114+
Issue.record("Expected a generic-member diagnostic, but resolution succeeded")
115+
} catch let error as BridgeJSCoreDiagnosticError {
116+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
117+
#expect(
118+
combined.contains("Generic @JS functions are only supported as top-level functions")
119+
)
120+
}
121+
}
122+
123+
@Test
124+
func fullyUnusedGenericParameterRejected() throws {
125+
do {
126+
_ = try resolveApp(
127+
source: """
128+
@JS public func combine<T: _BridgedSwiftGenericBridgeable, U: _BridgedSwiftGenericBridgeable>(_ a: T) -> T { a }
129+
"""
130+
)
131+
Issue.record("Expected a fully-unused-generic 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 genericParameterMustBeUsedAsParameter() throws {
140+
do {
141+
_ = try resolveApp(
142+
source: """
143+
@JS public func f<T: _BridgedSwiftGenericBridgeable>() -> T { fatalError() }
144+
"""
145+
)
146+
Issue.record("Expected a generic-parameter-usage 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 be used in at least one parameter"))
150+
}
151+
}
152+
153+
@Test
154+
func genericConcreteReturnUnsupported() throws {
155+
do {
156+
_ = try resolveApp(
157+
source: """
158+
@JS public func f<T: _BridgedSwiftGenericBridgeable>(_ v: T) -> String { "" }
159+
"""
160+
)
161+
Issue.record("Expected a concrete-return diagnostic, but resolution succeeded")
162+
} catch let error as BridgeJSCoreDiagnosticError {
163+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
164+
#expect(combined.contains("must return the generic type"))
165+
}
166+
}
167+
168+
// MARK: - Positive control
169+
170+
// MARK: - Utilities
171+
172+
private func resolveApp(source appSource: String) throws -> BridgeJSSkeleton {
173+
let swiftAPI = SwiftToSkeleton(
174+
progress: .silent,
175+
moduleName: "App",
176+
exposeToGlobal: false,
177+
externalModuleIndex: ExternalModuleIndex(dependencies: [])
178+
)
179+
let sourceFile = Parser.parse(source: appSource)
180+
swiftAPI.addSourceFile(sourceFile, inputFilePath: "App.swift")
181+
return try swiftAPI.finalize()
182+
}
183+
}

0 commit comments

Comments
 (0)