Skip to content

Commit 610b1a6

Browse files
BridgeJS: Improve diagnostics and fix-its for macros
Examples added/covered in this change: - @JSFunction: enforce throws(JSException) (missing or wrong type) with note + fix-it ("Declare throws(JSException)"). - @JSFunction: instance members outside @jsclass emit a clear diagnostic. - @JSGetter/@JSSetter: members (instance/static/class) outside @jsclass emit a clear diagnostic. - @JSSetter: invalid setter names (e.g. updateFoo) emit a diagnostic and suggest a rename fix-it (e.g. setFoo). - @JSSetter: missing value parameter emits a diagnostic and suggests adding a placeholder parameter. - @jsclass: using @jsclass on non-struct declarations emits a diagnostic; for "class" also suggests "Change 'class' to 'struct'".
1 parent bc752fc commit 610b1a6

File tree

10 files changed

+937
-89
lines changed

10 files changed

+937
-89
lines changed

Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,35 @@ extension JSClassMacro: MemberMacro {
1313
in context: some MacroExpansionContext
1414
) throws -> [DeclSyntax] {
1515
guard declaration.is(StructDeclSyntax.self) else {
16+
var fixIts: [FixIt] = []
17+
let note = Note(
18+
node: Syntax(declaration),
19+
message: JSMacroNoteMessage(
20+
message: "Use @JSClass on a struct wrapper to synthesize jsObject and JS bridging members."
21+
)
22+
)
23+
24+
if let classDecl = declaration.as(ClassDeclSyntax.self) {
25+
let structKeyword = classDecl.classKeyword.with(\.tokenKind, .keyword(.struct))
26+
fixIts.append(
27+
FixIt(
28+
message: JSMacroFixItMessage(message: "Change 'class' to 'struct'"),
29+
changes: [
30+
.replace(
31+
oldNode: Syntax(classDecl.classKeyword),
32+
newNode: Syntax(structKeyword)
33+
)
34+
]
35+
)
36+
)
37+
}
1638
context.diagnose(
17-
Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedJSClassDeclaration)
39+
Diagnostic(
40+
node: Syntax(declaration),
41+
message: JSMacroMessage.unsupportedJSClassDeclaration,
42+
notes: [note],
43+
fixIts: fixIts
44+
)
1845
)
1946
return []
2047
}

Plugins/BridgeJS/Sources/BridgeJSMacros/JSFunctionMacro.swift

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,17 @@ extension JSFunctionMacro: BodyMacro {
1414
if let functionDecl = declaration.as(FunctionDeclSyntax.self) {
1515
let enclosingTypeName = JSMacroHelper.enclosingTypeName(from: context)
1616
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+
)
1828

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

3545
let effects = functionDecl.signature.effectSpecifiers
3646
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)
3948

4049
let isVoid = JSMacroHelper.isVoidReturn(functionDecl.signature.returnClause?.type)
4150
let line = isVoid ? "\(prefix)\(call)" : "return \(prefix)\(call)"
@@ -45,28 +54,58 @@ extension JSFunctionMacro: BodyMacro {
4554
if let initializerDecl = declaration.as(InitializerDeclSyntax.self) {
4655
guard let enclosingTypeName = JSMacroHelper.enclosingTypeName(from: context) else {
4756
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+
)
4969
)
5070
return [CodeBlockItemSyntax(stringLiteral: "fatalError(\"@JSFunction init must be inside a type\")")]
5171
}
5272

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+
5380
let glueName = JSMacroHelper.glueName(baseName: "init", enclosingTypeName: enclosingTypeName)
5481
let parameters = initializerDecl.signature.parameterClause.parameters
5582
let arguments = JSMacroHelper.parameterNames(parameters)
5683
let call = "\(glueName)(\(arguments.joined(separator: ", ")))"
5784

5885
let effects = initializerDecl.signature.effectSpecifiers
5986
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)
6288

6389
return [
6490
CodeBlockItemSyntax(stringLiteral: "let jsObject = \(prefix)\(call)"),
6591
CodeBlockItemSyntax(stringLiteral: "self.init(unsafelyWrapping: jsObject)"),
6692
]
6793
}
6894

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+
)
70109
return []
71110
}
72111
}
@@ -82,7 +121,21 @@ extension JSFunctionMacro: PeerMacro {
82121
) throws -> [DeclSyntax] {
83122
if declaration.is(FunctionDeclSyntax.self) { return [] }
84123
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+
)
86139
return []
87140
}
88141
}

Plugins/BridgeJS/Sources/BridgeJSMacros/JSGetterMacro.swift

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,30 @@ extension JSGetterMacro: AccessorMacro {
1515
let binding = variableDecl.bindings.first,
1616
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)
1717
else {
18-
context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedVariable))
18+
context.diagnose(
19+
Diagnostic(
20+
node: Syntax(declaration),
21+
message: JSMacroMessage.unsupportedVariable,
22+
notes: [
23+
Note(
24+
node: Syntax(declaration),
25+
message: JSMacroNoteMessage(
26+
message: "@JSGetter must be attached to a single stored or computed property."
27+
)
28+
)
29+
]
30+
)
31+
)
1932
return []
2033
}
2134

2235
let enclosingTypeName = JSMacroHelper.enclosingTypeName(from: context)
2336
let isStatic = JSMacroHelper.isStatic(variableDecl.modifiers)
24-
let isInstanceMember = enclosingTypeName != nil && !isStatic
37+
let isTopLevel = enclosingTypeName == nil
38+
let isInstanceMember = !isTopLevel && !isStatic
39+
if !isTopLevel {
40+
JSMacroHelper.diagnoseMissingJSClass(node: node, for: "JSGetter", in: context)
41+
}
2542

2643
// Strip backticks from property name (e.g., "`prefix`" -> "prefix")
2744
// Backticks are only needed for Swift identifiers, not function names
@@ -71,7 +88,20 @@ extension JSGetterMacro: PeerMacro {
7188
in context: some MacroExpansionContext
7289
) throws -> [DeclSyntax] {
7390
guard declaration.is(VariableDeclSyntax.self) else {
74-
context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedVariable))
91+
context.diagnose(
92+
Diagnostic(
93+
node: Syntax(declaration),
94+
message: JSMacroMessage.unsupportedVariable,
95+
notes: [
96+
Note(
97+
node: Syntax(declaration),
98+
message: JSMacroNoteMessage(
99+
message: "@JSGetter must be attached to a single stored or computed property."
100+
)
101+
)
102+
]
103+
)
104+
)
75105
return []
76106
}
77107
return []

0 commit comments

Comments
 (0)