diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift index f39ac16f8..aa696ec31 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift @@ -1198,6 +1198,14 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { className: classNameForABI ) + if let mutatingModifier = node.modifiers.first(where: { $0.name.tokenKind == .keyword(.mutating) }) { + diagnose( + node: mutatingModifier, + message: "@JS does not support mutating struct methods: mutations to 'self' cannot be propagated back to JavaScript", + hint: "Remove the mutating keyword or redesign the API to return the updated value instead" + ) + return nil + } guard let effects = collectEffects(signature: node.signature, isStatic: isStatic) else { return nil } @@ -1522,7 +1530,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { } } - /// Walks extension members under the matching type’s state, returning whether the type was found. + /// Walks extension members under the matching type's state, returning whether the type was found. /// /// Note: The lookup scans dictionaries keyed by `makeKey(name:namespace:)`, matching only by /// plain name. If two types share a name but differ by namespace, `.first(where:)` picks diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index 346b7333b..d132888b3 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -635,11 +635,35 @@ public struct Effects: Codable, Equatable, Sendable { public var isAsync: Bool public var isThrows: Bool public var isStatic: Bool + public var isMutating: Bool - public init(isAsync: Bool, isThrows: Bool, isStatic: Bool = false) { + public init(isAsync: Bool, isThrows: Bool, isStatic: Bool = false, isMutating: Bool = false) { self.isAsync = isAsync self.isThrows = isThrows self.isStatic = isStatic + self.isMutating = isMutating + } + + private enum CodingKeys: String, CodingKey { + case isAsync, isThrows, isStatic, isMutating + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.isAsync = try container.decode(Bool.self, forKey: .isAsync) + self.isThrows = try container.decode(Bool.self, forKey: .isThrows) + self.isStatic = try container.decode(Bool.self, forKey: .isStatic) + self.isMutating = try container.decodeIfPresent(Bool.self, forKey: .isMutating) ?? false + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(isAsync, forKey: .isAsync) + try container.encode(isThrows, forKey: .isThrows) + try container.encode(isStatic, forKey: .isStatic) + if isMutating { + try container.encode(isMutating, forKey: .isMutating) + } } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/MutatingStructMethod.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/MutatingStructMethod.swift new file mode 100644 index 000000000..23d2e6538 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/MutatingStructMethod.swift @@ -0,0 +1,12 @@ +@JS struct Counter { + var number: Int +} + +extension Counter { + @JS public mutating func increment() { + number += 1 + } + @JS public mutating func add(_ value: Int) { + number += value + } +}