Skip to content

Commit e5ddc9f

Browse files
committed
BridgeJS: Add tests and fixtures for generic imports
1 parent ccd828b commit e5ddc9f

108 files changed

Lines changed: 5321 additions & 94 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ import Testing
141141
swiftParts.append(s)
142142
}
143143
}
144+
if let typeRegistration = GenericTypeRegistrationCodegen().render(for: skeleton) {
145+
swiftParts.append(typeRegistration)
146+
}
144147
let combinedSwift =
145148
swiftParts
146149
.map { $0.trimmingCharacters(in: .newlines) }

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,26 @@ import Testing
139139
try snapshot(bridgeJSLink: bridgeJSLink, name: "MixedModules")
140140
}
141141

142+
private func linkedJS(forFixture input: String) throws -> String {
143+
let url = Self.inputsDirectory.appendingPathComponent(input)
144+
let name = url.deletingPathExtension().lastPathComponent
145+
let sourceFile = Parser.parse(source: try String(contentsOf: url, encoding: .utf8))
146+
let importSwift = SwiftToSkeleton(
147+
progress: .silent,
148+
moduleName: "TestModule",
149+
exposeToGlobal: false,
150+
externalModuleIndex: .empty
151+
)
152+
importSwift.addSourceFile(sourceFile, inputFilePath: "\(name).swift")
153+
let importResult = try importSwift.finalize()
154+
var bridgeJSLink = BridgeJSLink(sharedMemory: false)
155+
let encoder = JSONEncoder()
156+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
157+
let unifiedData = try encoder.encode(importResult)
158+
try bridgeJSLink.addSkeletonFile(data: unifiedData)
159+
return try bridgeJSLink.link().0
160+
}
161+
142162
@Test
143163
func perClassIdentityModeFromAnnotation() throws {
144164
let url = Self.inputsDirectory.appendingPathComponent("IdentityModeClass.swift")
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 expectDiagnostic(
26+
source: String,
27+
moduleName: String = "App",
28+
contains message: String,
29+
sourceLocation: Testing.SourceLocation = #_sourceLocation
30+
) {
31+
do {
32+
_ = try makeSkeleton(source, moduleName: moduleName)
33+
Issue.record("Expected diagnostic but resolution succeeded", sourceLocation: sourceLocation)
34+
} catch let error as BridgeJSCoreDiagnosticError {
35+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
36+
#expect(combined.contains(message), sourceLocation: sourceLocation)
37+
} catch {
38+
Issue.record("Unexpected error: \(error)", sourceLocation: sourceLocation)
39+
}
40+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import Testing
2+
3+
@testable import BridgeJSCore
4+
5+
@Suite struct GenericExportDiagnosticsTests {
6+
7+
@Test
8+
func genericExportedFunctionRejected() {
9+
expectDiagnostic(
10+
source: """
11+
@JS public func identity<T: BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
12+
""",
13+
contains: "Generic parameters on exported @JS functions are not supported yet"
14+
)
15+
}
16+
17+
@Test
18+
func genericMethodOnExportedClassRejected() {
19+
expectDiagnostic(
20+
source: """
21+
@JS final class Box {
22+
@JS init() {}
23+
@JS func wrap<T: BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
24+
}
25+
""",
26+
contains: "Generic parameters on exported @JS functions are not supported yet"
27+
)
28+
}
29+
30+
@Test
31+
func genericMethodOnExportedStructRejected() {
32+
expectDiagnostic(
33+
source: """
34+
@JS struct Pair {
35+
@JS init() {}
36+
@JS func first<T: BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
37+
}
38+
""",
39+
contains: "Generic parameters on exported @JS functions are not supported yet"
40+
)
41+
}
42+
43+
@Test
44+
func genericStaticMethodOnExportedEnumRejected() {
45+
expectDiagnostic(
46+
source: """
47+
@JS enum Factory {
48+
case primary
49+
@JS static func one<T: BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
50+
}
51+
""",
52+
contains: "Generic parameters on exported @JS functions are not supported yet"
53+
)
54+
}
55+
56+
@Test
57+
func unconstrainedGenericExportedFunctionRejected() {
58+
expectDiagnostic(
59+
source: """
60+
@JS public func identity<T>(_ value: T) -> T { value }
61+
""",
62+
contains: "Generic parameters on exported @JS functions are not supported yet"
63+
)
64+
}
65+
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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+
11+
@Test
12+
func genericParameterRequiresBridgeableConstraint() {
13+
expectDiagnostic(
14+
source: """
15+
@JSFunction func identity<T>(_ value: T) throws(JSException) -> T
16+
""",
17+
contains: "Generic parameter 'T' must be constrained to 'BridgedSwiftGenericBridgeable'"
18+
)
19+
}
20+
21+
@Test
22+
func genericWhereClauseUnsupported() {
23+
expectDiagnostic(
24+
source: """
25+
@JSFunction func identity<T: BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T where T: Sendable
26+
""",
27+
contains: "'where' clauses are not supported on @JSFunction"
28+
)
29+
}
30+
31+
@Test
32+
func asyncGenericImportUnsupported() {
33+
expectDiagnostic(
34+
source: """
35+
@JSFunction func identityAsync<T: BridgedSwiftGenericBridgeable>(_ value: T) async throws(JSException) -> T
36+
""",
37+
contains: "Generic @JSFunction declarations cannot be 'async' yet."
38+
)
39+
}
40+
41+
@Test
42+
func genericImportedMethodIsParsed() throws {
43+
let skeleton = try makeSkeleton(
44+
"""
45+
@JSClass struct Box {
46+
@JSFunction func member<T: BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
47+
}
48+
""",
49+
moduleName: "App"
50+
)
51+
let imported = try #require(skeleton.imported)
52+
let types = imported.children.flatMap { $0.types }
53+
let box = try #require(types.first { $0.name == "Box" })
54+
let method = try #require(box.methods.first { $0.name == "member" })
55+
#expect(method.genericParameters == ["T"])
56+
}
57+
58+
@Test
59+
func genericImportedConstructorIsParsed() throws {
60+
let skeleton = try makeSkeleton(
61+
"""
62+
@JSClass struct Box {
63+
@JSFunction init<T: BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException)
64+
}
65+
""",
66+
moduleName: "App"
67+
)
68+
let imported = try #require(skeleton.imported)
69+
let types = imported.children.flatMap { $0.types }
70+
let box = try #require(types.first { $0.name == "Box" })
71+
let constructor = try #require(box.constructor)
72+
#expect(constructor.genericParameters == ["T"])
73+
#expect(constructor.parameters.map(\.type) == [.generic("T")])
74+
}
75+
76+
@Test
77+
func genericImportedConstructorUnconstrainedParamIsRejected() {
78+
expectDiagnostic(
79+
source: """
80+
@JSClass struct Box {
81+
@JSFunction init<T>(_ value: T) throws(JSException)
82+
}
83+
""",
84+
contains:
85+
"Generic parameter 'T' must be constrained to 'BridgedSwiftGenericBridgeable' to be used with @JSFunction."
86+
)
87+
}
88+
89+
@Test
90+
func genericImportedConstructorUnusedTypeParamIsRejected() {
91+
expectDiagnostic(
92+
source: """
93+
@JSClass struct Box {
94+
@JSFunction init<T: BridgedSwiftGenericBridgeable>(_ value: Int) throws(JSException)
95+
}
96+
""",
97+
contains:
98+
"The generic parameter 'T' must be used in a parameter of a generic @JSFunction initializer."
99+
)
100+
}
101+
102+
@Test
103+
func genericImportedConstructorAsyncIsRejected() {
104+
expectDiagnostic(
105+
source: """
106+
@JSClass struct Box {
107+
@JSFunction init<T: BridgedSwiftGenericBridgeable>(_ value: T) async throws(JSException)
108+
}
109+
""",
110+
contains: "Generic @JSFunction declarations cannot be 'async' yet."
111+
)
112+
}
113+
114+
@Test
115+
func genericImportedConstructorUnsupportedWrapperFormIsRejected() {
116+
expectDiagnostic(
117+
source: """
118+
@JSClass struct Box {
119+
@JSFunction init<T: BridgedSwiftGenericBridgeable>(_ value: [[T]]) throws(JSException)
120+
}
121+
""",
122+
contains: "may only be used as a bare type"
123+
)
124+
}
125+
126+
@Test(arguments: [
127+
("[[T]]", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: [[T]]) throws(JSException)"),
128+
("[T?]", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: [T?]) throws(JSException)"),
129+
("T??", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: T??) throws(JSException)"),
130+
("[Int: T]", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: [Int: T]) throws(JSException)"),
131+
])
132+
func unsupportedGenericWrapperFormsInParameter(label: String, source: String) {
133+
expectDiagnostic(
134+
source: source,
135+
contains: "may only be used as a bare type"
136+
)
137+
}
138+
139+
@Test(arguments: [
140+
("[[T]]", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: T) throws(JSException) -> [[T]]"),
141+
("[T?]", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: T) throws(JSException) -> [T?]"),
142+
("T??", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: T) throws(JSException) -> T??"),
143+
("[Int: T]", "@JSFunction func f<T: BridgedSwiftGenericBridgeable>(_ v: T) throws(JSException) -> [Int: T]"),
144+
])
145+
func unsupportedGenericWrapperFormsInReturn(label: String, source: String) {
146+
expectDiagnostic(
147+
source: source,
148+
contains: "may only be used as a bare type"
149+
)
150+
}
151+
152+
@Test
153+
func genericImportedMethodAsyncIsRejected() {
154+
expectDiagnostic(
155+
source: """
156+
@JSClass struct Box {
157+
@JSFunction func member<T: BridgedSwiftGenericBridgeable>(_ value: T) async throws(JSException) -> T
158+
}
159+
""",
160+
contains: "Generic @JSFunction declarations cannot be 'async' yet."
161+
)
162+
}
163+
164+
@Test
165+
func genericImportedMethodUnconstrainedParamIsRejected() {
166+
expectDiagnostic(
167+
source: """
168+
@JSClass struct Box {
169+
@JSFunction func member<T>(_ value: T) throws(JSException) -> T
170+
}
171+
""",
172+
contains:
173+
"Generic parameter 'T' must be constrained to 'BridgedSwiftGenericBridgeable' to be used with @JSFunction."
174+
)
175+
}
176+
177+
@Test
178+
func genericImportedFunctionUnusedTypeParamIsRejected() {
179+
expectDiagnostic(
180+
source: """
181+
@JSFunction func unused<T: BridgedSwiftGenericBridgeable>() throws(JSException) -> Int
182+
""",
183+
contains:
184+
"The generic parameter 'T' must be used in a parameter or return type of a generic @JSFunction declaration."
185+
)
186+
}
187+
188+
@Test
189+
func genericImportedMethodUnusedTypeParamIsRejected() {
190+
expectDiagnostic(
191+
source: """
192+
@JSClass struct Box {
193+
@JSFunction func member<T: BridgedSwiftGenericBridgeable>() throws(JSException) -> Int
194+
}
195+
""",
196+
contains:
197+
"The generic parameter 'T' must be used in a parameter or return type of a generic @JSFunction declaration."
198+
)
199+
}
200+
201+
@Test
202+
func genericImportedReturnOnlyTypeParamIsAllowed() throws {
203+
let skeleton = try makeSkeleton(
204+
"""
205+
@JSFunction func make<T: BridgedSwiftGenericBridgeable>() throws(JSException) -> T
206+
""",
207+
moduleName: "App"
208+
)
209+
let imported = try #require(skeleton.imported)
210+
let functions = imported.children.flatMap { $0.functions }
211+
let function = try #require(functions.first { $0.name == "make" })
212+
#expect(function.genericParameters == ["T"])
213+
}
214+
}

0 commit comments

Comments
 (0)