Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion 37 Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1843,11 +1843,46 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
}

override func visitPost(_ node: StructDeclSyntax) {
if case .structBody(_, _) = stateStack.current {
if case .structBody(_, let structKey) = stateStack.current {
stateStack.pop()
validateStructInitOrder(node: node, structKey: structKey)
}
}

private func validateStructInitOrder(node: StructDeclSyntax, structKey: String) {
guard let exportedStruct = exportedStructByName[structKey],
let constructor = exportedStruct.constructor
else {
// No explicit @JS init — synthesized memberwise init is assumed,
// which always matches declaration order.
return
}

let instanceProps = exportedStruct.properties.filter { !$0.isStatic }
let expectedLabels = instanceProps.map(\.name)
let actualLabels = constructor.parameters.compactMap(\.label)

guard expectedLabels != actualLabels else { return }

// Find the @JS init node so we can point the diagnostic at it.
let initNode: (any SyntaxProtocol) =
node.memberBlock.members
.compactMap { $0.decl.as(InitializerDeclSyntax.self) }
.first(where: { $0.attributes.hasJSAttribute() })
?? node

let expectedOrder = expectedLabels.joined(separator: ", ")
let actualOrder = actualLabels.joined(separator: ", ")

diagnose(
node: initNode,
message:
"@JS struct initializer parameters must match stored properties in declaration order. Expected (\(expectedOrder)), got (\(actualOrder))",
hint:
"Reorder the initializer parameters to match the property declaration order, or remove the @JS init to use the synthesized memberwise initializer"
)
}

private func visitProtocolMethod(
node: FunctionDeclSyntax,
protocolName: String,
Expand Down
71 changes: 71 additions & 0 deletions 71 Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftSyntax
import Testing

@testable import BridgeJSCore
@testable import BridgeJSSkeleton

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

// MARK: - Struct init order validation

@Test
func structInitMismatchedOrderProducesDiagnostic() throws {
let source = """
@JS struct Animal {
var size: Double
var age: Int

@JS init(age: Int, size: Double) {
self.age = age
self.size = size
}
}
"""
let swiftAPI = SwiftToSkeleton(
progress: .silent,
moduleName: "TestModule",
exposeToGlobal: false,
externalModuleIndex: .empty
)
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "test.swift")
#expect(throws: BridgeJSCoreDiagnosticError.self) {
_ = try swiftAPI.finalize()
}
}

@Test
func structInitMatchingOrderSucceeds() throws {
let source = """
@JS struct Point {
var x: Double
var y: Double

@JS init(x: Double, y: Double) {
self.x = x
self.y = y
}
}
"""
let swiftAPI = SwiftToSkeleton(
progress: .silent,
moduleName: "TestModule",
exposeToGlobal: false,
externalModuleIndex: .empty
)
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "test.swift")
let skeleton = try swiftAPI.finalize()
#expect(skeleton.exported != nil)
}

@Test
func structWithoutExplicitInitSucceeds() throws {
let source = """
@JS struct Point {
var x: Double
var y: Double
}
"""
let swiftAPI = SwiftToSkeleton(
progress: .silent,
moduleName: "TestModule",
exposeToGlobal: false,
externalModuleIndex: .empty
)
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "test.swift")
let skeleton = try swiftAPI.finalize()
#expect(skeleton.exported != nil)
}

@Test
func omitsNextLineWhenErrorIsOnLastLine() throws {
let source = """
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.