Skip to content

Commit e60402c

Browse files
committed
BridgeJS: Diagnose struct initializer parameter order mismatch
1 parent 44ebc38 commit e60402c

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
@@ -1818,11 +1818,46 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
18181818
}
18191819

18201820
override func visitPost(_ node: StructDeclSyntax) {
1821-
if case .structBody(_, _) = stateStack.current {
1821+
if case .structBody(_, let structKey) = stateStack.current {
18221822
stateStack.pop()
1823+
validateStructInitOrder(node: node, structKey: structKey)
18231824
}
18241825
}
18251826

1827+
private func validateStructInitOrder(node: StructDeclSyntax, structKey: String) {
1828+
guard let exportedStruct = exportedStructByName[structKey],
1829+
let constructor = exportedStruct.constructor
1830+
else {
1831+
// No explicit @JS init — synthesized memberwise init is assumed,
1832+
// which always matches declaration order.
1833+
return
1834+
}
1835+
1836+
let instanceProps = exportedStruct.properties.filter { !$0.isStatic }
1837+
let expectedLabels = instanceProps.map(\.name)
1838+
let actualLabels = constructor.parameters.compactMap(\.label)
1839+
1840+
guard expectedLabels != actualLabels else { return }
1841+
1842+
// Find the @JS init node so we can point the diagnostic at it.
1843+
let initNode: (any SyntaxProtocol) =
1844+
node.memberBlock.members
1845+
.compactMap { $0.decl.as(InitializerDeclSyntax.self) }
1846+
.first(where: { $0.attributes.hasJSAttribute() })
1847+
?? node
1848+
1849+
let expectedOrder = expectedLabels.joined(separator: ", ")
1850+
let actualOrder = actualLabels.joined(separator: ", ")
1851+
1852+
diagnose(
1853+
node: initNode,
1854+
message:
1855+
"@JS struct initializer parameters must match stored properties in declaration order. Expected (\(expectedOrder)), got (\(actualOrder))",
1856+
hint:
1857+
"Reorder the initializer parameters to match the property declaration order, or remove the @JS init to use the synthesized memberwise initializer"
1858+
)
1859+
}
1860+
18261861
private func visitProtocolMethod(
18271862
node: FunctionDeclSyntax,
18281863
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.
@@ -165,6 +166,76 @@ import Testing
165166
#expect(description.contains("<stdin>:2:"))
166167
}
167168

169+
// MARK: - Struct init order validation
170+
171+
@Test
172+
func structInitMismatchedOrderProducesDiagnostic() throws {
173+
let source = """
174+
@JS struct Animal {
175+
var size: Double
176+
var age: Int
177+
178+
@JS init(age: Int, size: Double) {
179+
self.age = age
180+
self.size = size
181+
}
182+
}
183+
"""
184+
let swiftAPI = SwiftToSkeleton(
185+
progress: .silent,
186+
moduleName: "TestModule",
187+
exposeToGlobal: false,
188+
externalModuleIndex: .empty
189+
)
190+
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "test.swift")
191+
#expect(throws: BridgeJSCoreDiagnosticError.self) {
192+
_ = try swiftAPI.finalize()
193+
}
194+
}
195+
196+
@Test
197+
func structInitMatchingOrderSucceeds() throws {
198+
let source = """
199+
@JS struct Point {
200+
var x: Double
201+
var y: Double
202+
203+
@JS init(x: Double, y: Double) {
204+
self.x = x
205+
self.y = y
206+
}
207+
}
208+
"""
209+
let swiftAPI = SwiftToSkeleton(
210+
progress: .silent,
211+
moduleName: "TestModule",
212+
exposeToGlobal: false,
213+
externalModuleIndex: .empty
214+
)
215+
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "test.swift")
216+
let skeleton = try swiftAPI.finalize()
217+
#expect(skeleton.exported != nil)
218+
}
219+
220+
@Test
221+
func structWithoutExplicitInitSucceeds() throws {
222+
let source = """
223+
@JS struct Point {
224+
var x: Double
225+
var y: Double
226+
}
227+
"""
228+
let swiftAPI = SwiftToSkeleton(
229+
progress: .silent,
230+
moduleName: "TestModule",
231+
exposeToGlobal: false,
232+
externalModuleIndex: .empty
233+
)
234+
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "test.swift")
235+
let skeleton = try swiftAPI.finalize()
236+
#expect(skeleton.exported != nil)
237+
}
238+
168239
@Test
169240
func omitsNextLineWhenErrorIsOnLastLine() throws {
170241
let source = """

0 commit comments

Comments
 (0)