Skip to content
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
57 changes: 55 additions & 2 deletions Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -73,7 +88,7 @@ extension JSClassMacro: MemberMacro {
members.append(
DeclSyntax(
"""
init(unsafelyWrapping jsObject: JSObject) {
\(raw: memberAccessModifier)init(unsafelyWrapping jsObject: JSObject) {
self.jsObject = jsObject
}
"""
Expand All @@ -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,
Expand All @@ -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
}
}
4 changes: 4 additions & 0 deletions Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
98 changes: 98 additions & 0 deletions Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
131 changes: 131 additions & 0 deletions Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading