@@ -14,7 +14,17 @@ extension JSFunctionMacro: BodyMacro { |
14 | 14 | if let functionDecl = declaration.as(FunctionDeclSyntax.self) { |
15 | 15 | let enclosingTypeName = JSMacroHelper.enclosingTypeName(from: context) |
16 | 16 | let isStatic = JSMacroHelper.isStatic(functionDecl.modifiers) |
17 | | - let isInstanceMember = enclosingTypeName != nil && !isStatic |
| 17 | + let isTopLevel = enclosingTypeName == nil |
| 18 | + let isInstanceMember = !isTopLevel && !isStatic |
| 19 | + if !isTopLevel { |
| 20 | + JSMacroHelper.diagnoseMissingJSClass(node: node, for: "JSFunction", in: context) |
| 21 | + } |
| 22 | + |
| 23 | + JSMacroHelper.diagnoseThrowsRequiresJSException( |
| 24 | + signature: functionDecl.signature, |
| 25 | + on: Syntax(functionDecl), |
| 26 | + in: context |
| 27 | + ) |
18 | 28 |
|
19 | 29 | // Strip backticks from function name (e.g., "`prefix`" -> "prefix") |
20 | 30 | // Backticks are only needed for Swift identifiers, not function names |
@@ -34,8 +44,7 @@ extension JSFunctionMacro: BodyMacro { |
34 | 44 |
|
35 | 45 | let effects = functionDecl.signature.effectSpecifiers |
36 | 46 | let isAsync = effects?.asyncSpecifier != nil |
37 | | - let isThrows = effects?.throwsClause != nil |
38 | | - let prefix = JSMacroHelper.tryAwaitPrefix(isAsync: isAsync, isThrows: isThrows) |
| 47 | + let prefix = JSMacroHelper.tryAwaitPrefix(isAsync: isAsync, isThrows: true) |
39 | 48 |
|
40 | 49 | let isVoid = JSMacroHelper.isVoidReturn(functionDecl.signature.returnClause?.type) |
41 | 50 | let line = isVoid ? "\(prefix)\(call)" : "return \(prefix)\(call)" |
@@ -45,28 +54,58 @@ extension JSFunctionMacro: BodyMacro { |
45 | 54 | if let initializerDecl = declaration.as(InitializerDeclSyntax.self) { |
46 | 55 | guard let enclosingTypeName = JSMacroHelper.enclosingTypeName(from: context) else { |
47 | 56 | context.diagnose( |
48 | | - Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedDeclaration) |
| 57 | + Diagnostic( |
| 58 | + node: Syntax(declaration), |
| 59 | + message: JSMacroMessage.unsupportedDeclaration, |
| 60 | + notes: [ |
| 61 | + Note( |
| 62 | + node: Syntax(declaration), |
| 63 | + message: JSMacroNoteMessage( |
| 64 | + message: "Move this initializer inside a JS wrapper type annotated with @JSClass." |
| 65 | + ) |
| 66 | + ) |
| 67 | + ] |
| 68 | + ) |
49 | 69 | ) |
50 | 70 | return [CodeBlockItemSyntax(stringLiteral: "fatalError(\"@JSFunction init must be inside a type\")")] |
51 | 71 | } |
52 | 72 |
|
| 73 | + JSMacroHelper.diagnoseMissingJSClass(node: node, for: "JSFunction", in: context) |
| 74 | + JSMacroHelper.diagnoseThrowsRequiresJSException( |
| 75 | + signature: initializerDecl.signature, |
| 76 | + on: Syntax(initializerDecl), |
| 77 | + in: context |
| 78 | + ) |
| 79 | + |
53 | 80 | let glueName = JSMacroHelper.glueName(baseName: "init", enclosingTypeName: enclosingTypeName) |
54 | 81 | let parameters = initializerDecl.signature.parameterClause.parameters |
55 | 82 | let arguments = JSMacroHelper.parameterNames(parameters) |
56 | 83 | let call = "\(glueName)(\(arguments.joined(separator: ", ")))" |
57 | 84 |
|
58 | 85 | let effects = initializerDecl.signature.effectSpecifiers |
59 | 86 | let isAsync = effects?.asyncSpecifier != nil |
60 | | - let isThrows = effects?.throwsClause != nil |
61 | | - let prefix = JSMacroHelper.tryAwaitPrefix(isAsync: isAsync, isThrows: isThrows) |
| 87 | + let prefix = JSMacroHelper.tryAwaitPrefix(isAsync: isAsync, isThrows: true) |
62 | 88 |
|
63 | 89 | return [ |
64 | 90 | CodeBlockItemSyntax(stringLiteral: "let jsObject = \(prefix)\(call)"), |
65 | 91 | CodeBlockItemSyntax(stringLiteral: "self.init(unsafelyWrapping: jsObject)"), |
66 | 92 | ] |
67 | 93 | } |
68 | 94 |
|
69 | | - context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedDeclaration)) |
| 95 | + context.diagnose( |
| 96 | + Diagnostic( |
| 97 | + node: Syntax(declaration), |
| 98 | + message: JSMacroMessage.unsupportedDeclaration, |
| 99 | + notes: [ |
| 100 | + Note( |
| 101 | + node: Syntax(declaration), |
| 102 | + message: JSMacroNoteMessage( |
| 103 | + message: "Apply @JSFunction to a function or initializer on your @JSClass wrapper type." |
| 104 | + ) |
| 105 | + ) |
| 106 | + ] |
| 107 | + ) |
| 108 | + ) |
70 | 109 | return [] |
71 | 110 | } |
72 | 111 | } |
@@ -82,7 +121,21 @@ extension JSFunctionMacro: PeerMacro { |
82 | 121 | ) throws -> [DeclSyntax] { |
83 | 122 | if declaration.is(FunctionDeclSyntax.self) { return [] } |
84 | 123 | if declaration.is(InitializerDeclSyntax.self) { return [] } |
85 | | - context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedDeclaration)) |
| 124 | + context.diagnose( |
| 125 | + Diagnostic( |
| 126 | + node: Syntax(declaration), |
| 127 | + message: JSMacroMessage.unsupportedDeclaration, |
| 128 | + notes: [ |
| 129 | + Note( |
| 130 | + node: Syntax(declaration), |
| 131 | + message: JSMacroNoteMessage( |
| 132 | + message: |
| 133 | + "Place @JSFunction on a function or initializer; use @JSGetter/@JSSetter for properties." |
| 134 | + ) |
| 135 | + ) |
| 136 | + ] |
| 137 | + ) |
| 138 | + ) |
86 | 139 | return [] |
87 | 140 | } |
88 | 141 | } |