Skip to content

Commit 6d5a6f3

Browse files
Implemented strict-yet-compatible throws handling for macros:
- JSFunction/JSSetter now always generate throwing bodies but allow `throws`, `throws(Error)`, or `throws(JSException)` without diagnostics; missing or wrong typed throws get an error + fix-it to insert `throws(JSException)`. - Shared helpers build throws clauses and propagation notes; setter keeps its own note text. - JSFunction tests updated to require try in generated bodies, add missing-throws coverage, and adjust wrong-type cases; new missing-throws diagnostic test added. JSSetter tests updated with accepted throws(Error) and wrong-type coverage. - All macro tests pass: `swift test --package-path ./Plugins/BridgeJS --filter BridgeJSMacrosTests`. Note: pushing wasn’t possible from here (no remote/credentials).
1 parent d2351e2 commit 6d5a6f3

5 files changed

Lines changed: 58 additions & 19 deletions

File tree

Plugins/BridgeJS/Sources/BridgeJSMacros/JSFunctionMacro.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ extension JSFunctionMacro: BodyMacro {
4040

4141
let effects = functionDecl.signature.effectSpecifiers
4242
let isAsync = effects?.asyncSpecifier != nil
43-
let isThrows = effects?.throwsClause != nil
44-
let prefix = JSMacroHelper.tryAwaitPrefix(isAsync: isAsync, isThrows: isThrows)
43+
let prefix = JSMacroHelper.tryAwaitPrefix(isAsync: isAsync, isThrows: true)
4544

4645
let isVoid = JSMacroHelper.isVoidReturn(functionDecl.signature.returnClause?.type)
4746
let line = isVoid ? "\(prefix)\(call)" : "return \(prefix)\(call)"
@@ -80,8 +79,7 @@ extension JSFunctionMacro: BodyMacro {
8079

8180
let effects = initializerDecl.signature.effectSpecifiers
8281
let isAsync = effects?.asyncSpecifier != nil
83-
let isThrows = effects?.throwsClause != nil
84-
let prefix = JSMacroHelper.tryAwaitPrefix(isAsync: isAsync, isThrows: isThrows)
82+
let prefix = JSMacroHelper.tryAwaitPrefix(isAsync: isAsync, isThrows: true)
8583

8684
return [
8785
CodeBlockItemSyntax(stringLiteral: "let jsObject = \(prefix)\(call)"),

Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,14 @@ enum JSMacroHelper {
202202
in context: some MacroExpansionContext,
203203
additionalNotes: [Note] = []
204204
) {
205-
let throwsTypeName = signature.effectSpecifiers?.throwsClause?.type?.as(IdentifierTypeSyntax.self)?.name.text
205+
let throwsClause = signature.effectSpecifiers?.throwsClause
206+
let throwsTypeName = throwsClause?.type?.as(IdentifierTypeSyntax.self)?.name.text
206207
let isJSException = throwsTypeName == "JSException"
208+
let isAllowedGenericError = throwsClause != nil && (throwsTypeName == nil || throwsTypeName == "Error" || throwsTypeName == "Swift.Error")
207209
guard !isJSException else { return }
210+
guard !isAllowedGenericError else { return }
208211

209-
let newThrowsClause = jsExceptionThrowsClause(from: signature.effectSpecifiers?.throwsClause)
212+
let newThrowsClause = jsExceptionThrowsClause(from: throwsClause)
210213

211214
let fixIt: FixIt
212215
if let throwsClause = signature.effectSpecifiers?.throwsClause {
@@ -215,11 +218,14 @@ enum JSMacroHelper {
215218
changes: [.replace(oldNode: Syntax(throwsClause), newNode: Syntax(newThrowsClause))]
216219
)
217220
} else {
221+
let adjustedParameterClause = signature.parameterClause.with(\.rightParen.trailingTrivia, .spaces(0))
218222
let newEffects = FunctionEffectSpecifiersSyntax(
219223
asyncSpecifier: signature.effectSpecifiers?.asyncSpecifier,
220224
throwsClause: newThrowsClause
221225
)
222-
let newSignature = signature.with(\.effectSpecifiers, newEffects)
226+
let newSignature = signature
227+
.with(\.parameterClause, adjustedParameterClause)
228+
.with(\.effectSpecifiers, newEffects)
223229
fixIt = FixIt(
224230
message: JSMacroFixItMessage(message: "Declare throws(JSException)"),
225231
changes: [.replace(oldNode: Syntax(signature), newNode: Syntax(newSignature))]
@@ -267,6 +273,7 @@ enum JSMacroHelper {
267273
static func jsExceptionThrowsClause(from throwsClause: ThrowsClauseSyntax?) -> ThrowsClauseSyntax {
268274
let throwsSpecifier = (throwsClause?.throwsSpecifier ?? .keyword(.throws, leadingTrivia: .space))
269275
.with(\.trailingTrivia, .spaces(0))
276+
.with(\.leadingTrivia, throwsClause?.throwsSpecifier.leadingTrivia ?? .space)
270277
let leftParen = throwsClause?.leftParen ?? .leftParenToken()
271278
let rightParen = (throwsClause?.rightParen ?? .rightParenToken())
272279
.with(\.trailingTrivia, throwsClause?.rightParen?.trailingTrivia ?? .space)

Plugins/BridgeJS/Sources/BridgeJSMacros/JSSetterMacro.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,14 +189,16 @@ extension JSSetterMacro: BodyMacro {
189189
arguments.append(paramName.text)
190190

191191
// Ensure throws(JSException) is declared to match the generated body.
192-
let existingThrowsType = functionDecl.signature.effectSpecifiers?.throwsClause?.type
192+
let existingThrowsClause = functionDecl.signature.effectSpecifiers?.throwsClause
193+
let existingThrowsType = existingThrowsClause?.type
193194
.flatMap { $0.as(IdentifierTypeSyntax.self)?.name.text }
194195
let hasTypedJSException = existingThrowsType == "JSException"
195-
let hasAnyThrows = functionDecl.signature.effectSpecifiers?.throwsClause != nil
196+
let isAllowedGenericError = existingThrowsClause != nil && (existingThrowsType == nil || existingThrowsType == "Error" || existingThrowsType == "Swift.Error")
197+
let hasAnyThrows = existingThrowsClause != nil
196198

197-
if !hasTypedJSException {
199+
if !hasTypedJSException && !isAllowedGenericError {
198200
let throwsClause = JSMacroHelper.jsExceptionThrowsClause(
199-
from: functionDecl.signature.effectSpecifiers?.throwsClause
201+
from: existingThrowsClause
200202
)
201203
let newEffects = (functionDecl.signature.effectSpecifiers
202204
?? FunctionEffectSpecifiersSyntax(asyncSpecifier: nil, throwsClause: nil))

Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSFunctionMacroTests.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,28 @@ import BridgeJSMacros
102102
return try _$parse(json)
103103
}
104104
""",
105+
macroSpecs: macroSpecs,
106+
indentationWidth: indentationWidth,
107+
)
108+
}
109+
110+
@Test func topLevelFunctionThrowsWrongType() {
111+
TestSupport.assertMacroExpansion(
112+
"""
113+
@JSFunction
114+
func parse(json: String) throws(CustomError) -> [String: Any]
115+
""",
116+
expandedSource: """
117+
func parse(json: String) throws(CustomError) -> [String: Any] {
118+
return try _$parse(json)
119+
}
120+
""",
105121
diagnostics: [
106122
DiagnosticSpec(
107123
message: "@JSFunction throws must be declared as throws(JSException).",
108124
line: 1,
109125
column: 1,
126+
severity: .error,
110127
notes: [
111128
NoteSpec(
112129
message: "@JSFunction must propagate JavaScript errors as JSException.",
@@ -129,23 +146,22 @@ import BridgeJSMacros
129146
)
130147
}
131148

132-
@Test func topLevelFunctionThrowsWrongType() {
149+
@Test func topLevelFunctionMissingThrowsClause() {
133150
TestSupport.assertMacroExpansion(
134151
"""
135152
@JSFunction
136-
func parse(json: String) throws(CustomError) -> [String: Any]
153+
func greet(name: String) -> String
137154
""",
138155
expandedSource: """
139-
func parse(json: String) throws(CustomError) -> [String: Any] {
140-
return try _$parse(json)
156+
func greet(name: String) -> String {
157+
return try _$greet(name)
141158
}
142159
""",
143160
diagnostics: [
144161
DiagnosticSpec(
145162
message: "@JSFunction throws must be declared as throws(JSException).",
146163
line: 1,
147164
column: 1,
148-
severity: .error,
149165
notes: [
150166
NoteSpec(
151167
message: "@JSFunction must propagate JavaScript errors as JSException.",
@@ -162,7 +178,7 @@ import BridgeJSMacros
162178
applyFixIts: ["Declare throws(JSException)"],
163179
fixedSource: """
164180
@JSFunction
165-
func parse(json: String) throws(JSException) -> [String: Any]
181+
func greet(name: String) throws(JSException) -> String
166182
""",
167183
indentationWidth: indentationWidth,
168184
)

Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSSetterMacroTests.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,10 +309,10 @@ import BridgeJSMacros
309309
TestSupport.assertMacroExpansion(
310310
"""
311311
@JSSetter
312-
func setFoo(_ value: Foo) throws(Error)
312+
func setFoo(_ value: Foo) throws(CustomError)
313313
""",
314314
expandedSource: """
315-
func setFoo(_ value: Foo) throws(Error) {
315+
func setFoo(_ value: Foo) throws(CustomError) {
316316
try _$foo_set(value)
317317
}
318318
""",
@@ -343,6 +343,22 @@ import BridgeJSMacros
343343
)
344344
}
345345

346+
@Test func setterThrowsErrorAccepted() {
347+
TestSupport.assertMacroExpansion(
348+
"""
349+
@JSSetter
350+
func setFoo(_ value: Foo) throws(Error)
351+
""",
352+
expandedSource: """
353+
func setFoo(_ value: Foo) throws(Error) {
354+
try _$foo_set(value)
355+
}
356+
""",
357+
macroSpecs: macroSpecs,
358+
indentationWidth: indentationWidth,
359+
)
360+
}
361+
346362
@Test func unsupportedDeclaration() {
347363
TestSupport.assertMacroExpansion(
348364
"""

0 commit comments

Comments
 (0)