Skip to content

Commit f90fe98

Browse files
committed
BridgeJS: Diagnose struct initializer parameter order mismatch
1 parent 2dd870e commit f90fe98

2 files changed

Lines changed: 107 additions & 1 deletion

File tree

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1843,11 +1843,46 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
18431843
}
18441844

18451845
override func visitPost(_ node: StructDeclSyntax) {
1846-
if case .structBody(_, _) = stateStack.current {
1846+
if case .structBody(_, let structKey) = stateStack.current {
18471847
stateStack.pop()
1848+
validateStructInitOrder(node: node, structKey: structKey)
18481849
}
18491850
}
18501851

1852+
private func validateStructInitOrder(node: StructDeclSyntax, structKey: String) {
1853+
guard let exportedStruct = exportedStructByName[structKey],
1854+
let constructor = exportedStruct.constructor
1855+
else {
1856+
// No explicit @JS init — synthesized memberwise init is assumed,
1857+
// which always matches declaration order.
1858+
return
1859+
}
1860+
1861+
let instanceProps = exportedStruct.properties.filter { !$0.isStatic }
1862+
let expectedLabels = instanceProps.map(\.name)
1863+
let actualLabels = constructor.parameters.compactMap(\.label)
1864+
1865+
guard expectedLabels != actualLabels else { return }
1866+
1867+
// Find the @JS init node so we can point the diagnostic at it.
1868+
let initNode: (any SyntaxProtocol) =
1869+
node.memberBlock.members
1870+
.compactMap { $0.decl.as(InitializerDeclSyntax.self) }
1871+
.first(where: { $0.attributes.hasJSAttribute() })
1872+
?? node
1873+
1874+
let expectedOrder = expectedLabels.joined(separator: ", ")
1875+
let actualOrder = actualLabels.joined(separator: ", ")
1876+
1877+
diagnose(
1878+
node: initNode,
1879+
message:
1880+
"@JS struct initializer parameters must match stored properties in declaration order. Expected (\(expectedOrder)), got (\(actualOrder))",
1881+
hint:
1882+
"Reorder the initializer parameters to match the property declaration order, or remove the @JS init to use the synthesized memberwise initializer"
1883+
)
1884+
}
1885+
18511886
private func visitProtocolMethod(
18521887
node: FunctionDeclSyntax,
18531888
protocolName: String,

Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import SwiftSyntax
33
import Testing
44

55
@testable import BridgeJSCore
6+
@testable import BridgeJSSkeleton
67

78
@Suite struct DiagnosticsTests {
89
/// Returns the first parameter's type node from a function in the source (the first `@JS func`-like decl), for pinpointing diagnostics.
@@ -234,6 +235,76 @@ import Testing
234235
#expect(skeleton.exported != nil)
235236
}
236237

238+
// MARK: - Struct init order validation
239+
240+
@Test
241+
func structInitMismatchedOrderProducesDiagnostic() throws {
242+
let source = """
243+
@JS struct Animal {
244+
var size: Double
245+
var age: Int
246+
247+
@JS init(age: Int, size: Double) {
248+
self.age = age
249+
self.size = size
250+
}
251+
}
252+
"""
253+
let swiftAPI = SwiftToSkeleton(
254+
progress: .silent,
255+
moduleName: "TestModule",
256+
exposeToGlobal: false,
257+
externalModuleIndex: .empty
258+
)
259+
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "test.swift")
260+
#expect(throws: BridgeJSCoreDiagnosticError.self) {
261+
_ = try swiftAPI.finalize()
262+
}
263+
}
264+
265+
@Test
266+
func structInitMatchingOrderSucceeds() throws {
267+
let source = """
268+
@JS struct Point {
269+
var x: Double
270+
var y: Double
271+
272+
@JS init(x: Double, y: Double) {
273+
self.x = x
274+
self.y = y
275+
}
276+
}
277+
"""
278+
let swiftAPI = SwiftToSkeleton(
279+
progress: .silent,
280+
moduleName: "TestModule",
281+
exposeToGlobal: false,
282+
externalModuleIndex: .empty
283+
)
284+
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "test.swift")
285+
let skeleton = try swiftAPI.finalize()
286+
#expect(skeleton.exported != nil)
287+
}
288+
289+
@Test
290+
func structWithoutExplicitInitSucceeds() throws {
291+
let source = """
292+
@JS struct Point {
293+
var x: Double
294+
var y: Double
295+
}
296+
"""
297+
let swiftAPI = SwiftToSkeleton(
298+
progress: .silent,
299+
moduleName: "TestModule",
300+
exposeToGlobal: false,
301+
externalModuleIndex: .empty
302+
)
303+
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "test.swift")
304+
let skeleton = try swiftAPI.finalize()
305+
#expect(skeleton.exported != nil)
306+
}
307+
237308
@Test
238309
func omitsNextLineWhenErrorIsOnLastLine() throws {
239310
let source = """

0 commit comments

Comments
 (0)