From 4d7b7733a6e63328b0174627ad0a0c25a96e4b0c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 17 Feb 2026 16:10:46 +0900 Subject: [PATCH] BridgeJS: Fix `@JSClass` on public/package access level struct Also cover more access levels in `@JSFunction` tests. --- .../Sources/BridgeJSMacros/JSClassMacro.swift | 57 +++++++- .../BridgeJSMacros/JSMacroSupport.swift | 4 + .../JSClassMacroTests.swift | 98 +++++++++++++ .../Generated/BridgeJS.swift | 131 +++++++++++++++++ .../Generated/JavaScript/BridgeJS.json | 132 ++++++++++++++++++ .../JSClassCompileTests.swift | 11 ++ .../JSFunctionCompileTests.swift | 7 + 7 files changed, 438 insertions(+), 2 deletions(-) create mode 100644 Tests/BridgeJSRuntimeTests/MacroCompileTests/JSClassCompileTests.swift create mode 100644 Tests/BridgeJSRuntimeTests/MacroCompileTests/JSFunctionCompileTests.swift diff --git a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift index 79c3105ee..c44655c7e 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift @@ -47,6 +47,21 @@ extension JSClassMacro: MemberMacro { } var members: [DeclSyntax] = [] + guard let structDecl = declaration.as(StructDeclSyntax.self) else { return members } + + if let accessLevel = accessLevel(from: structDecl.modifiers), + accessLevel == .private || accessLevel == .fileprivate + { + context.diagnose( + Diagnostic( + node: Syntax(structDecl), + message: JSMacroMessage.jsClassRequiresAtLeastInternal + ) + ) + return [] + } + + let memberAccessModifier = synthesizedMemberAccessModifier(for: declaration).map { "\($0) " } ?? "" let existingMembers = declaration.memberBlock.members let hasJSObjectProperty = existingMembers.contains { member in @@ -57,7 +72,7 @@ extension JSClassMacro: MemberMacro { } if !hasJSObjectProperty { - members.append(DeclSyntax("let jsObject: JSObject")) + members.append(DeclSyntax("\(raw: memberAccessModifier)let jsObject: JSObject")) } let hasUnsafelyWrappingInit = existingMembers.contains { member in @@ -73,7 +88,7 @@ extension JSClassMacro: MemberMacro { members.append( DeclSyntax( """ - init(unsafelyWrapping jsObject: JSObject) { + \(raw: memberAccessModifier)init(unsafelyWrapping jsObject: JSObject) { self.jsObject = jsObject } """ @@ -95,6 +110,11 @@ extension JSClassMacro: ExtensionMacro { ) throws -> [ExtensionDeclSyntax] { guard let structDecl = declaration.as(StructDeclSyntax.self) else { return [] } guard !protocols.isEmpty else { return [] } + if let accessLevel = accessLevel(from: structDecl.modifiers), + accessLevel == .private || accessLevel == .fileprivate + { + return [] + } // Do not add extension if the struct already conforms to _JSBridgedClass if let clause = structDecl.inheritanceClause, @@ -108,4 +128,37 @@ extension JSClassMacro: ExtensionMacro { try ExtensionDeclSyntax("extension \(type.trimmed): \(raw: conformanceList) {}") ] } + + private static func synthesizedMemberAccessModifier(for declaration: some DeclGroupSyntax) -> String? { + guard let structDecl = declaration.as(StructDeclSyntax.self) else { return nil } + switch accessLevel(from: structDecl.modifiers) { + case .public: return "public" + case .package: return "package" + case .internal: return "internal" + case .fileprivate, .private, .none: return nil + } + } + + private enum AccessLevel { + case `public` + case package + case `internal` + case `fileprivate` + case `private` + } + + private static func accessLevel(from modifiers: DeclModifierListSyntax?) -> AccessLevel? { + guard let modifiers else { return nil } + for modifier in modifiers { + switch modifier.name.tokenKind { + case .keyword(.public): return .public + case .keyword(.package): return .package + case .keyword(.internal): return .internal + case .keyword(.fileprivate): return .fileprivate + case .keyword(.private): return .private + default: continue + } + } + return nil + } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift index c30d91dd1..224462ddb 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift @@ -14,6 +14,10 @@ enum JSMacroMessage: String, DiagnosticMessage { case setterRequiresThrows = "@JSSetter function must declare throws(JSException)." case jsFunctionRequiresThrows = "@JSFunction throws must be declared as throws(JSException)." case requiresJSClass = "JavaScript members must be declared inside a @JSClass struct." + case jsClassRequiresAtLeastInternal = + "@JSClass does not support private/fileprivate access level. Use internal, package, or public." + case jsClassMemberRequiresAtLeastInternal = + "@JSClass requires jsObject and init(unsafelyWrapping:) to be at least internal." var message: String { rawValue } var diagnosticID: MessageID { MessageID(domain: "JavaScriptKitMacros", id: rawValue) } diff --git a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift index 86a1f6b7c..77dc814eb 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift @@ -370,4 +370,102 @@ import BridgeJSMacros indentationWidth: indentationWidth ) } + + @Test func publicStructSynthesizesPublicMembers() { + TestSupport.assertMacroExpansion( + """ + @JSClass + public struct MyClass { + } + """, + expandedSource: """ + public struct MyClass { + + public let jsObject: JSObject + + public init(unsafelyWrapping jsObject: JSObject) { + self.jsObject = jsObject + } + } + + extension MyClass: _JSBridgedClass { + } + """, + macroSpecs: macroSpecs, + indentationWidth: indentationWidth + ) + } + + @Test func packageStructSynthesizesPackageMembers() { + TestSupport.assertMacroExpansion( + """ + @JSClass + package struct MyClass { + } + """, + expandedSource: """ + package struct MyClass { + + package let jsObject: JSObject + + package init(unsafelyWrapping jsObject: JSObject) { + self.jsObject = jsObject + } + } + + extension MyClass: _JSBridgedClass { + } + """, + macroSpecs: macroSpecs, + indentationWidth: indentationWidth + ) + } + + @Test func privateStructIsRejected() { + TestSupport.assertMacroExpansion( + """ + @JSClass + private struct MyClass { + } + """, + expandedSource: """ + private struct MyClass { + } + """, + diagnostics: [ + DiagnosticSpec( + message: + "@JSClass does not support private/fileprivate access level. Use internal, package, or public.", + line: 1, + column: 1 + ) + ], + macroSpecs: macroSpecs, + indentationWidth: indentationWidth + ) + } + + @Test func fileprivateStructIsRejected() { + TestSupport.assertMacroExpansion( + """ + @JSClass + fileprivate struct MyClass { + } + """, + expandedSource: """ + fileprivate struct MyClass { + } + """, + diagnostics: [ + DiagnosticSpec( + message: + "@JSClass does not support private/fileprivate access level. Use internal, package, or public.", + line: 1, + column: 1 + ) + ], + macroSpecs: macroSpecs, + indentationWidth: indentationWidth + ) + } } diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index 96ca9be2e..78c9014d7 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -10156,6 +10156,137 @@ func _$jsTranslatePoint(_ point: Point, _ dx: Int, _ dy: Int) throws(JSException return Point.bridgeJSLiftReturn(ret) } +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_MyJSClassInternal_init") +fileprivate func bjs_MyJSClassInternal_init() -> Int32 +#else +fileprivate func bjs_MyJSClassInternal_init() -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +func _$MyJSClassInternal_init() throws(JSException) -> JSObject { + let ret = bjs_MyJSClassInternal_init() + if let error = _swift_js_take_exception() { + throw error + } + return JSObject.bridgeJSLiftReturn(ret) +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_MyJSClassPublic_init") +fileprivate func bjs_MyJSClassPublic_init() -> Int32 +#else +fileprivate func bjs_MyJSClassPublic_init() -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +func _$MyJSClassPublic_init() throws(JSException) -> JSObject { + let ret = bjs_MyJSClassPublic_init() + if let error = _swift_js_take_exception() { + throw error + } + return JSObject.bridgeJSLiftReturn(ret) +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_MyJSClassPackage_init") +fileprivate func bjs_MyJSClassPackage_init() -> Int32 +#else +fileprivate func bjs_MyJSClassPackage_init() -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +func _$MyJSClassPackage_init() throws(JSException) -> JSObject { + let ret = bjs_MyJSClassPackage_init() + if let error = _swift_js_take_exception() { + throw error + } + return JSObject.bridgeJSLiftReturn(ret) +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsFunctionWithPackageAccess") +fileprivate func bjs_jsFunctionWithPackageAccess() -> Void +#else +fileprivate func bjs_jsFunctionWithPackageAccess() -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsFunctionWithPackageAccess() throws(JSException) -> Void { + bjs_jsFunctionWithPackageAccess() + if let error = _swift_js_take_exception() { + throw error + } +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsFunctionWithPublicAccess") +fileprivate func bjs_jsFunctionWithPublicAccess() -> Void +#else +fileprivate func bjs_jsFunctionWithPublicAccess() -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsFunctionWithPublicAccess() throws(JSException) -> Void { + bjs_jsFunctionWithPublicAccess() + if let error = _swift_js_take_exception() { + throw error + } +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsFunctionWithInternalAccess") +fileprivate func bjs_jsFunctionWithInternalAccess() -> Void +#else +fileprivate func bjs_jsFunctionWithInternalAccess() -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsFunctionWithInternalAccess() throws(JSException) -> Void { + bjs_jsFunctionWithInternalAccess() + if let error = _swift_js_take_exception() { + throw error + } +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsFunctionWithFilePrivateAccess") +fileprivate func bjs_jsFunctionWithFilePrivateAccess() -> Void +#else +fileprivate func bjs_jsFunctionWithFilePrivateAccess() -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsFunctionWithFilePrivateAccess() throws(JSException) -> Void { + bjs_jsFunctionWithFilePrivateAccess() + if let error = _swift_js_take_exception() { + throw error + } +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsFunctionWithPrivateAccess") +fileprivate func bjs_jsFunctionWithPrivateAccess() -> Void +#else +fileprivate func bjs_jsFunctionWithPrivateAccess() -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsFunctionWithPrivateAccess() throws(JSException) -> Void { + bjs_jsFunctionWithPrivateAccess() + if let error = _swift_js_take_exception() { + throw error + } +} + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_OptionalSupportImports_jsRoundTripOptionalNumberNull_static") fileprivate func bjs_OptionalSupportImports_jsRoundTripOptionalNumberNull_static(_ valueIsSome: Int32, _ valueValue: Int32) -> Void diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json index 9c086c808..2cd992ea4 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json @@ -15297,6 +15297,138 @@ { "functions" : [ + ], + "types" : [ + { + "constructor" : { + "parameters" : [ + + ] + }, + "from" : "global", + "getters" : [ + + ], + "methods" : [ + + ], + "name" : "MyJSClassInternal", + "setters" : [ + + ], + "staticMethods" : [ + + ] + }, + { + "constructor" : { + "parameters" : [ + + ] + }, + "from" : "global", + "getters" : [ + + ], + "methods" : [ + + ], + "name" : "MyJSClassPublic", + "setters" : [ + + ], + "staticMethods" : [ + + ] + }, + { + "constructor" : { + "parameters" : [ + + ] + }, + "from" : "global", + "getters" : [ + + ], + "methods" : [ + + ], + "name" : "MyJSClassPackage", + "setters" : [ + + ], + "staticMethods" : [ + + ] + } + ] + }, + { + "functions" : [ + { + "name" : "jsFunctionWithPackageAccess", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "name" : "jsFunctionWithPublicAccess", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "name" : "jsFunctionWithInternalAccess", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "name" : "jsFunctionWithFilePrivateAccess", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "name" : "jsFunctionWithPrivateAccess", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + } + ], + "types" : [ + + ] + }, + { + "functions" : [ + ], "types" : [ { diff --git a/Tests/BridgeJSRuntimeTests/MacroCompileTests/JSClassCompileTests.swift b/Tests/BridgeJSRuntimeTests/MacroCompileTests/JSClassCompileTests.swift new file mode 100644 index 000000000..05a23d9de --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/MacroCompileTests/JSClassCompileTests.swift @@ -0,0 +1,11 @@ +import JavaScriptKit + +@JSClass(from: .global) internal struct MyJSClassInternal { + @JSFunction init() throws(JSException) +} +@JSClass(from: .global) public struct MyJSClassPublic { + @JSFunction init() throws(JSException) +} +@JSClass(from: .global) package struct MyJSClassPackage { + @JSFunction init() throws(JSException) +} diff --git a/Tests/BridgeJSRuntimeTests/MacroCompileTests/JSFunctionCompileTests.swift b/Tests/BridgeJSRuntimeTests/MacroCompileTests/JSFunctionCompileTests.swift new file mode 100644 index 000000000..e74c84d51 --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/MacroCompileTests/JSFunctionCompileTests.swift @@ -0,0 +1,7 @@ +import JavaScriptKit + +@JSFunction package func jsFunctionWithPackageAccess() throws(JSException) +@JSFunction public func jsFunctionWithPublicAccess() throws(JSException) +@JSFunction internal func jsFunctionWithInternalAccess() throws(JSException) +@JSFunction fileprivate func jsFunctionWithFilePrivateAccess() throws(JSException) +@JSFunction private func jsFunctionWithPrivateAccess() throws(JSException)