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
29 changes: 28 additions & 1 deletion Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,35 @@ extension JSClassMacro: MemberMacro {
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard declaration.is(StructDeclSyntax.self) else {
var fixIts: [FixIt] = []
let note = Note(
node: Syntax(declaration),
message: JSMacroNoteMessage(
message: "Use @JSClass on a struct wrapper to synthesize jsObject and JS bridging members."
)
)

if let classDecl = declaration.as(ClassDeclSyntax.self) {
let structKeyword = classDecl.classKeyword.with(\.tokenKind, .keyword(.struct))
fixIts.append(
FixIt(
message: JSMacroFixItMessage(message: "Change 'class' to 'struct'"),
changes: [
.replace(
oldNode: Syntax(classDecl.classKeyword),
newNode: Syntax(structKeyword)
)
]
)
)
}
context.diagnose(
Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedJSClassDeclaration)
Diagnostic(
node: Syntax(declaration),
message: JSMacroMessage.unsupportedJSClassDeclaration,
notes: [note],
fixIts: fixIts
)
)
return []
}
Expand Down
69 changes: 61 additions & 8 deletions Plugins/BridgeJS/Sources/BridgeJSMacros/JSFunctionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,17 @@ extension JSFunctionMacro: BodyMacro {
if let functionDecl = declaration.as(FunctionDeclSyntax.self) {
let enclosingTypeName = JSMacroHelper.enclosingTypeName(from: context)
let isStatic = JSMacroHelper.isStatic(functionDecl.modifiers)
let isInstanceMember = enclosingTypeName != nil && !isStatic
let isTopLevel = enclosingTypeName == nil
let isInstanceMember = !isTopLevel && !isStatic
if !isTopLevel {
JSMacroHelper.diagnoseMissingJSClass(node: node, for: "JSFunction", in: context)
}

JSMacroHelper.diagnoseThrowsRequiresJSException(
signature: functionDecl.signature,
on: Syntax(functionDecl),
in: context
)

// Strip backticks from function name (e.g., "`prefix`" -> "prefix")
// Backticks are only needed for Swift identifiers, not function names
Expand All @@ -34,8 +44,7 @@ extension JSFunctionMacro: BodyMacro {

let effects = functionDecl.signature.effectSpecifiers
let isAsync = effects?.asyncSpecifier != nil
let isThrows = effects?.throwsClause != nil
let prefix = JSMacroHelper.tryAwaitPrefix(isAsync: isAsync, isThrows: isThrows)
let prefix = JSMacroHelper.tryAwaitPrefix(isAsync: isAsync, isThrows: true)

let isVoid = JSMacroHelper.isVoidReturn(functionDecl.signature.returnClause?.type)
let line = isVoid ? "\(prefix)\(call)" : "return \(prefix)\(call)"
Expand All @@ -45,28 +54,58 @@ extension JSFunctionMacro: BodyMacro {
if let initializerDecl = declaration.as(InitializerDeclSyntax.self) {
guard let enclosingTypeName = JSMacroHelper.enclosingTypeName(from: context) else {
context.diagnose(
Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedDeclaration)
Diagnostic(
node: Syntax(declaration),
message: JSMacroMessage.unsupportedDeclaration,
notes: [
Note(
node: Syntax(declaration),
message: JSMacroNoteMessage(
message: "Move this initializer inside a JS wrapper type annotated with @JSClass."
)
)
]
)
)
return [CodeBlockItemSyntax(stringLiteral: "fatalError(\"@JSFunction init must be inside a type\")")]
}

JSMacroHelper.diagnoseMissingJSClass(node: node, for: "JSFunction", in: context)
JSMacroHelper.diagnoseThrowsRequiresJSException(
signature: initializerDecl.signature,
on: Syntax(initializerDecl),
in: context
)

let glueName = JSMacroHelper.glueName(baseName: "init", enclosingTypeName: enclosingTypeName)
let parameters = initializerDecl.signature.parameterClause.parameters
let arguments = JSMacroHelper.parameterNames(parameters)
let call = "\(glueName)(\(arguments.joined(separator: ", ")))"

let effects = initializerDecl.signature.effectSpecifiers
let isAsync = effects?.asyncSpecifier != nil
let isThrows = effects?.throwsClause != nil
let prefix = JSMacroHelper.tryAwaitPrefix(isAsync: isAsync, isThrows: isThrows)
let prefix = JSMacroHelper.tryAwaitPrefix(isAsync: isAsync, isThrows: true)

return [
CodeBlockItemSyntax(stringLiteral: "let jsObject = \(prefix)\(call)"),
CodeBlockItemSyntax(stringLiteral: "self.init(unsafelyWrapping: jsObject)"),
]
}

context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedDeclaration))
context.diagnose(
Diagnostic(
node: Syntax(declaration),
message: JSMacroMessage.unsupportedDeclaration,
notes: [
Note(
node: Syntax(declaration),
message: JSMacroNoteMessage(
message: "Apply @JSFunction to a function or initializer on your @JSClass wrapper type."
)
)
]
)
)
return []
}
}
Expand All @@ -82,7 +121,21 @@ extension JSFunctionMacro: PeerMacro {
) throws -> [DeclSyntax] {
if declaration.is(FunctionDeclSyntax.self) { return [] }
if declaration.is(InitializerDeclSyntax.self) { return [] }
context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedDeclaration))
context.diagnose(
Diagnostic(
node: Syntax(declaration),
message: JSMacroMessage.unsupportedDeclaration,
notes: [
Note(
node: Syntax(declaration),
message: JSMacroNoteMessage(
message:
"Place @JSFunction on a function or initializer; use @JSGetter/@JSSetter for properties."
)
)
]
)
)
return []
}
}
36 changes: 33 additions & 3 deletions Plugins/BridgeJS/Sources/BridgeJSMacros/JSGetterMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,30 @@ extension JSGetterMacro: AccessorMacro {
let binding = variableDecl.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)
else {
context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedVariable))
context.diagnose(
Diagnostic(
node: Syntax(declaration),
message: JSMacroMessage.unsupportedVariable,
notes: [
Note(
node: Syntax(declaration),
message: JSMacroNoteMessage(
message: "@JSGetter must be attached to a single stored or computed property."
)
)
]
)
)
return []
}

let enclosingTypeName = JSMacroHelper.enclosingTypeName(from: context)
let isStatic = JSMacroHelper.isStatic(variableDecl.modifiers)
let isInstanceMember = enclosingTypeName != nil && !isStatic
let isTopLevel = enclosingTypeName == nil
let isInstanceMember = !isTopLevel && !isStatic
if !isTopLevel {
JSMacroHelper.diagnoseMissingJSClass(node: node, for: "JSGetter", in: context)
}

// Strip backticks from property name (e.g., "`prefix`" -> "prefix")
// Backticks are only needed for Swift identifiers, not function names
Expand Down Expand Up @@ -71,7 +88,20 @@ extension JSGetterMacro: PeerMacro {
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard declaration.is(VariableDeclSyntax.self) else {
context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedVariable))
context.diagnose(
Diagnostic(
node: Syntax(declaration),
message: JSMacroMessage.unsupportedVariable,
notes: [
Note(
node: Syntax(declaration),
message: JSMacroNoteMessage(
message: "@JSGetter must be attached to a single stored or computed property."
)
)
]
)
)
return []
}
return []
Expand Down
Loading