From 610b1a60bde271d90d7a2d74b94fedac909dd35e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 5 Feb 2026 09:39:48 +0900 Subject: [PATCH] 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'". --- .../Sources/BridgeJSMacros/JSClassMacro.swift | 29 +- .../BridgeJSMacros/JSFunctionMacro.swift | 69 ++++- .../BridgeJSMacros/JSGetterMacro.swift | 36 ++- .../BridgeJSMacros/JSMacroSupport.swift | 188 ++++++++++++- .../BridgeJSMacros/JSSetterMacro.swift | 162 ++++++++++- .../JSClassMacroTests.swift | 30 +- .../JSFunctionMacroTests.swift | 261 ++++++++++++++---- .../JSGetterMacroTests.swift | 60 +++- .../JSSetterMacroTests.swift | 189 ++++++++++++- Sources/JavaScriptKit/Macros.swift | 2 +- 10 files changed, 937 insertions(+), 89 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift index 2641df4b..79c3105e 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift @@ -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 [] } diff --git a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSFunctionMacro.swift b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSFunctionMacro.swift index a51bf10c..0fff3546 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSFunctionMacro.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSFunctionMacro.swift @@ -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 @@ -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)" @@ -45,11 +54,29 @@ 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) @@ -57,8 +84,7 @@ extension JSFunctionMacro: BodyMacro { 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)"), @@ -66,7 +92,20 @@ extension JSFunctionMacro: BodyMacro { ] } - 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 [] } } @@ -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 [] } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSGetterMacro.swift b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSGetterMacro.swift index 44c3620c..a3b9435e 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSGetterMacro.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSGetterMacro.swift @@ -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 @@ -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 [] diff --git a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift index 59fa1c4c..c30d91dd 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSMacroSupport.swift @@ -11,26 +11,41 @@ enum JSMacroMessage: String, DiagnosticMessage { case invalidSetterName = "@JSSetter function name must start with 'set' followed by a property name (e.g., 'setFoo')." case setterRequiresParameter = "@JSSetter function must have at least one parameter." + 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." var message: String { rawValue } var diagnosticID: MessageID { MessageID(domain: "JavaScriptKitMacros", id: rawValue) } var severity: DiagnosticSeverity { .error } } +struct JSMacroNoteMessage: NoteMessage { + let message: String + var noteID: MessageID { MessageID(domain: "JavaScriptKitMacros", id: message) } +} + +struct JSMacroFixItMessage: FixItMessage { + let message: String + var fixItID: MessageID { MessageID(domain: "JavaScriptKitMacros", id: message) } +} + +enum JSMacroText { + static let jsExceptionPropagation = "@JSFunction must propagate JavaScript errors as JSException." + static let jsSetterExceptionPropagation = "@JSSetter must propagate JavaScript errors as JSException." +} + enum JSMacroHelper { static func enclosingTypeName(from context: some MacroExpansionContext) -> String? { + enclosingTypeSyntax(from: context).flatMap(typeName(of:)) + } + + static func enclosingTypeSyntax(from context: some MacroExpansionContext) -> Syntax? { for syntax in context.lexicalContext { - if let decl = syntax.as(ClassDeclSyntax.self) { - return decl.name.text - } - if let decl = syntax.as(StructDeclSyntax.self) { - return decl.name.text - } - if let decl = syntax.as(EnumDeclSyntax.self) { - return decl.name.text - } - if let decl = syntax.as(ActorDeclSyntax.self) { - return decl.name.text + if syntax.is(ClassDeclSyntax.self) || syntax.is(StructDeclSyntax.self) + || syntax.is(EnumDeclSyntax.self) || syntax.is(ActorDeclSyntax.self) + { + return Syntax(syntax) } } return nil @@ -98,4 +113,155 @@ enum JSMacroHelper { } return name } + + static func capitalizingFirstLetter(_ value: String) -> String { + guard let first = value.first else { return value } + return first.uppercased() + value.dropFirst() + } + + static func hasJSClassAttribute(_ typeSyntax: Syntax) -> Bool { + guard let attributes = attributes(of: typeSyntax) else { return false } + for attribute in attributes.compactMap({ $0.as(AttributeSyntax.self) }) { + let name = attribute.attributeName.trimmedDescription + if name == "JSClass" || name == "@JSClass" { + return true + } + } + return false + } + + static func typeName(of syntax: Syntax) -> String? { + switch syntax.as(SyntaxEnum.self) { + case .structDecl(let decl): return decl.name.text + case .classDecl(let decl): return decl.name.text + case .enumDecl(let decl): return decl.name.text + case .actorDecl(let decl): return decl.name.text + default: return nil + } + } + + private static func attributes(of syntax: Syntax) -> AttributeListSyntax? { + switch syntax.as(SyntaxEnum.self) { + case .structDecl(let decl): return decl.attributes + case .classDecl(let decl): return decl.attributes + case .enumDecl(let decl): return decl.attributes + case .actorDecl(let decl): return decl.attributes + default: return nil + } + } + + static func diagnoseThrowsRequiresJSException( + signature: FunctionSignatureSyntax, + on node: Syntax, + in context: some MacroExpansionContext, + additionalNotes: [Note] = [] + ) { + let throwsClause = signature.effectSpecifiers?.throwsClause + let throwsTypeName = throwsClause?.type?.as(IdentifierTypeSyntax.self)?.name.text + let isJSException = throwsTypeName == "JSException" + let isAllowedGenericError = + throwsClause != nil + && (throwsTypeName == nil || throwsTypeName == "Error" || throwsTypeName == "Swift.Error") + guard !isJSException else { return } + guard !isAllowedGenericError else { return } + + let newThrowsClause = jsExceptionThrowsClause(from: throwsClause) + + let fixIt: FixIt + if let throwsClause = signature.effectSpecifiers?.throwsClause { + fixIt = FixIt( + message: JSMacroFixItMessage(message: "Declare throws(JSException)"), + changes: [.replace(oldNode: Syntax(throwsClause), newNode: Syntax(newThrowsClause))] + ) + } else { + let adjustedParameterClause = signature.parameterClause.with(\.rightParen.trailingTrivia, .spaces(0)) + let newEffects = FunctionEffectSpecifiersSyntax( + asyncSpecifier: signature.effectSpecifiers?.asyncSpecifier, + throwsClause: newThrowsClause + ) + let newSignature = + signature + .with(\.parameterClause, adjustedParameterClause) + .with(\.effectSpecifiers, newEffects) + fixIt = FixIt( + message: JSMacroFixItMessage(message: "Declare throws(JSException)"), + changes: [.replace(oldNode: Syntax(signature), newNode: Syntax(newSignature))] + ) + } + + var notes: [Note] = [ + jsExceptionPropagationNote(on: node) + ] + notes.append(contentsOf: additionalNotes) + + context.diagnose( + Diagnostic( + node: node, + message: JSMacroMessage.jsFunctionRequiresThrows, + notes: notes, + fixIts: [fixIt] + ) + ) + } + + static func diagnoseMissingJSClass( + node: some SyntaxProtocol, + for macroName: String, + in context: some MacroExpansionContext + ) { + guard let typeSyntax = enclosingTypeSyntax(from: context) else { return } + guard !hasJSClassAttribute(typeSyntax) else { return } + context.diagnose(Diagnostic(node: node, message: JSMacroMessage.requiresJSClass)) + } + + static func setterPropertyBase(from parameter: FunctionParameterSyntax?) -> String? { + guard let parameter else { return nil } + let candidateNames = [ + parameter.secondName, + parameter.firstName.tokenKind == .wildcard ? nil : parameter.firstName, + ].compactMap { $0?.text }.filter { $0 != "_" } + if let explicitName = candidateNames.first(where: { name in name != "value" && name != "newValue" }) { + return explicitName + } + if let identifierType = parameter.type.as(IdentifierTypeSyntax.self) { + let typeName = identifierType.name.text + guard let first = typeName.first else { return nil } + return first.lowercased() + typeName.dropFirst() + } + return candidateNames.first + } + + static func suggestedSetterName(rawFunctionName: String, firstParameter: FunctionParameterSyntax?) -> String? { + guard let base = setterPropertyBase(from: firstParameter) else { return nil } + return "set" + capitalizingFirstLetter(base) + } + + /// Build a typed throws(JSException) clause preserving trivia when possible. + static func jsExceptionThrowsClause(from throwsClause: ThrowsClauseSyntax?) -> ThrowsClauseSyntax { + let throwsSpecifier = (throwsClause?.throwsSpecifier ?? .keyword(.throws, leadingTrivia: .space)) + .with(\.trailingTrivia, .spaces(0)) + .with(\.leadingTrivia, throwsClause?.throwsSpecifier.leadingTrivia ?? .space) + let leftParen = throwsClause?.leftParen ?? .leftParenToken() + let rightParen = (throwsClause?.rightParen ?? .rightParenToken()) + .with(\.trailingTrivia, throwsClause?.rightParen?.trailingTrivia ?? .space) + + return ThrowsClauseSyntax( + throwsSpecifier: throwsSpecifier, + leftParen: leftParen, + type: TypeSyntax(IdentifierTypeSyntax(name: .identifier("JSException"))), + rightParen: rightParen + ) + } + + static func jsExceptionPropagationNote( + on node: Syntax, + message: String = JSMacroText.jsExceptionPropagation + ) -> Note { + Note( + node: node, + message: JSMacroNoteMessage( + message: message + ) + ) + } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSSetterMacro.swift b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSSetterMacro.swift index 3c728007..ba171e8e 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSSetterMacro.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSSetterMacro.swift @@ -13,18 +13,86 @@ extension JSSetterMacro: BodyMacro { ) throws -> [CodeBlockItemSyntax] { guard let functionDecl = declaration.as(FunctionDeclSyntax.self) else { context.diagnose( - Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedSetterDeclaration) + Diagnostic( + node: Syntax(declaration), + message: JSMacroMessage.unsupportedSetterDeclaration, + notes: [ + Note( + node: Syntax(declaration), + message: JSMacroNoteMessage( + message: "@JSSetter can only be used on methods that set a JavaScript property." + ) + ) + ] + ) ) return [] } let functionName = functionDecl.name.text + let parameters = functionDecl.signature.parameterClause.parameters + let suggestedSetterName = JSMacroHelper.suggestedSetterName( + rawFunctionName: JSMacroHelper.stripBackticks(functionName), + firstParameter: parameters.first + ) + let renameFixIts: [FixIt] + if let name = suggestedSetterName { + let replacement = functionDecl.name.with(\.tokenKind, .identifier(name)) + .with(\.leadingTrivia, functionDecl.name.leadingTrivia) + .with(\.trailingTrivia, functionDecl.name.trailingTrivia) + renameFixIts = [ + FixIt( + message: JSMacroFixItMessage(message: "Rename setter to '\(name)'"), + changes: [.replace(oldNode: Syntax(functionDecl.name), newNode: Syntax(replacement))] + ) + ] + } else { + renameFixIts = [] + } + let addParameterFixIts: [FixIt] = { + let placeholderParameter = FunctionParameterSyntax( + firstName: .wildcardToken(trailingTrivia: .space), + secondName: .identifier("value"), + colon: .colonToken(trailingTrivia: .space), + type: TypeSyntax(stringLiteral: "<#Type#>") + ) + let newClause = FunctionParameterClauseSyntax( + leftParen: .leftParenToken(), + parameters: FunctionParameterListSyntax([placeholderParameter]), + rightParen: .rightParenToken(trailingTrivia: .space) + ) + return [ + FixIt( + message: JSMacroFixItMessage(message: "Add a value parameter to the setter"), + changes: [ + .replace( + oldNode: Syntax(functionDecl.signature.parameterClause), + newNode: Syntax(newClause) + ) + ] + ) + ] + }() // Extract property name from setter function name (e.g., "setFoo" -> "foo") // Strip backticks if present (e.g., "set`prefix`" -> "prefix") let rawFunctionName = JSMacroHelper.stripBackticks(functionName) guard rawFunctionName.hasPrefix("set"), rawFunctionName.count > 3 else { - context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.invalidSetterName)) + context.diagnose( + Diagnostic( + node: Syntax(declaration), + message: JSMacroMessage.invalidSetterName, + notes: [ + Note( + node: Syntax(functionDecl.name), + message: JSMacroNoteMessage( + message: "Setter names must start with 'set' followed by the property name." + ) + ) + ], + fixIts: renameFixIts + ) + ) return [ CodeBlockItemSyntax( stringLiteral: @@ -35,7 +103,21 @@ extension JSSetterMacro: BodyMacro { let propertyName = String(rawFunctionName.dropFirst(3)) guard !propertyName.isEmpty else { - context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.invalidSetterName)) + context.diagnose( + Diagnostic( + node: Syntax(declaration), + message: JSMacroMessage.invalidSetterName, + notes: [ + Note( + node: Syntax(functionDecl.name), + message: JSMacroNoteMessage( + message: "Setter names must include the property after 'set'." + ) + ) + ], + fixIts: renameFixIts + ) + ) return [ CodeBlockItemSyntax( stringLiteral: @@ -49,7 +131,11 @@ extension JSSetterMacro: BodyMacro { 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: "JSSetter", in: context) + } let glueName = JSMacroHelper.glueName( baseName: baseName, @@ -63,9 +149,22 @@ extension JSSetterMacro: BodyMacro { } // Get the parameter name(s) - setters typically have one parameter - let parameters = functionDecl.signature.parameterClause.parameters guard let firstParam = parameters.first else { - context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.setterRequiresParameter)) + context.diagnose( + Diagnostic( + node: Syntax(declaration), + message: JSMacroMessage.setterRequiresParameter, + notes: [ + Note( + node: Syntax(declaration), + message: JSMacroNoteMessage( + message: "@JSSetter needs a parameter for the value being assigned." + ) + ) + ], + fixIts: addParameterFixIts + ) + ) return [ CodeBlockItemSyntax( stringLiteral: "fatalError(\"@JSSetter function must have at least one parameter\")" @@ -76,6 +175,44 @@ extension JSSetterMacro: BodyMacro { let paramName = firstParam.secondName ?? firstParam.firstName arguments.append(paramName.text) + // Ensure throws(JSException) is declared to match the generated body. + let existingThrowsClause = functionDecl.signature.effectSpecifiers?.throwsClause + let existingThrowsType = existingThrowsClause?.type + .flatMap { $0.as(IdentifierTypeSyntax.self)?.name.text } + let hasTypedJSException = existingThrowsType == "JSException" + let isAllowedGenericError = + existingThrowsClause != nil + && (existingThrowsType == nil || existingThrowsType == "Error" || existingThrowsType == "Swift.Error") + + if !hasTypedJSException && !isAllowedGenericError { + let throwsClause = JSMacroHelper.jsExceptionThrowsClause( + from: existingThrowsClause + ) + let newEffects = + (functionDecl.signature.effectSpecifiers + ?? FunctionEffectSpecifiersSyntax(asyncSpecifier: nil, throwsClause: nil)) + .with(\.throwsClause, throwsClause) + let newSignature = functionDecl.signature.with(\.effectSpecifiers, newEffects) + let fixIt = FixIt( + message: JSMacroFixItMessage(message: "Declare throws(JSException)"), + changes: [.replace(oldNode: Syntax(functionDecl.signature), newNode: Syntax(newSignature))] + ) + let notes: [Note] = [ + JSMacroHelper.jsExceptionPropagationNote( + on: Syntax(functionDecl), + message: JSMacroText.jsSetterExceptionPropagation + ) + ] + context.diagnose( + Diagnostic( + node: Syntax(functionDecl), + message: JSMacroMessage.setterRequiresThrows, + notes: notes, + fixIts: [fixIt] + ) + ) + } + let argsJoined = arguments.joined(separator: ", ") let call = "\(glueName)(\(argsJoined))" @@ -94,7 +231,18 @@ extension JSSetterMacro: PeerMacro { ) throws -> [DeclSyntax] { guard declaration.is(FunctionDeclSyntax.self) else { context.diagnose( - Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedSetterDeclaration) + Diagnostic( + node: Syntax(declaration), + message: JSMacroMessage.unsupportedSetterDeclaration, + notes: [ + Note( + node: Syntax(declaration), + message: JSMacroNoteMessage( + message: "@JSSetter should be attached to a method that writes a JavaScript property." + ) + ) + ] + ) ) return [] } diff --git a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift index 7640916e..86a1f6b7 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift @@ -159,7 +159,17 @@ import BridgeJSMacros DiagnosticSpec( message: "@JSClass can only be applied to structs.", line: 1, - column: 1 + column: 1, + notes: [ + NoteSpec( + message: "Use @JSClass on a struct wrapper to synthesize jsObject and JS bridging members.", + line: 1, + column: 1 + ) + ], + fixIts: [ + FixItSpec(message: "Change 'class' to 'struct'") + ] ) ], macroSpecs: macroSpecs, @@ -182,7 +192,14 @@ import BridgeJSMacros DiagnosticSpec( message: "@JSClass can only be applied to structs.", line: 1, - column: 1 + column: 1, + notes: [ + NoteSpec( + message: "Use @JSClass on a struct wrapper to synthesize jsObject and JS bridging members.", + line: 1, + column: 1 + ) + ] ) ], macroSpecs: macroSpecs, @@ -205,7 +222,14 @@ import BridgeJSMacros DiagnosticSpec( message: "@JSClass can only be applied to structs.", line: 1, - column: 1 + column: 1, + notes: [ + NoteSpec( + message: "Use @JSClass on a struct wrapper to synthesize jsObject and JS bridging members.", + line: 1, + column: 1 + ) + ] ) ], macroSpecs: macroSpecs, diff --git a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSFunctionMacroTests.swift b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSFunctionMacroTests.swift index 756935e7..28eade95 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSFunctionMacroTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSFunctionMacroTests.swift @@ -15,13 +15,40 @@ import BridgeJSMacros TestSupport.assertMacroExpansion( """ @JSFunction - func greet(name: String) -> String + func greet(name: String) throws(JSException) -> String """, expandedSource: """ - func greet(name: String) -> String { - return _$greet(name) + func greet(name: String) throws(JSException) -> String { + return try _$greet(name) + } + """, + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, + ) + } + + @Test func instanceMethodRequiresJSClass() { + TestSupport.assertMacroExpansion( + """ + struct MyClass { + @JSFunction + func getName() throws(JSException) -> String + } + """, + expandedSource: """ + struct MyClass { + func getName() throws(JSException) -> String { + return try _$MyClass_getName(self.jsObject) + } } """, + diagnostics: [ + DiagnosticSpec( + message: "JavaScript members must be declared inside a @JSClass struct.", + line: 2, + column: 5 + ) + ], macroSpecs: macroSpecs, indentationWidth: indentationWidth, ) @@ -31,11 +58,11 @@ import BridgeJSMacros TestSupport.assertMacroExpansion( """ @JSFunction - func log(message: String) + func log(message: String) throws(JSException) """, expandedSource: """ - func log(message: String) { - _$log(message) + func log(message: String) throws(JSException) { + try _$log(message) } """, macroSpecs: macroSpecs, @@ -47,11 +74,11 @@ import BridgeJSMacros TestSupport.assertMacroExpansion( """ @JSFunction - func log(message: String) -> Void + func log(message: String) throws(JSException) -> Void """, expandedSource: """ - func log(message: String) -> Void { - _$log(message) + func log(message: String) throws(JSException) -> Void { + try _$log(message) } """, macroSpecs: macroSpecs, @@ -63,11 +90,11 @@ import BridgeJSMacros TestSupport.assertMacroExpansion( """ @JSFunction - func log(message: String) -> () + func log(message: String) throws(JSException) -> () """, expandedSource: """ - func log(message: String) -> () { - _$log(message) + func log(message: String) throws(JSException) -> () { + try _$log(message) } """, macroSpecs: macroSpecs, @@ -76,6 +103,22 @@ import BridgeJSMacros } @Test func topLevelFunctionThrows() { + TestSupport.assertMacroExpansion( + """ + @JSFunction + func parse(json: String) throws(JSException) -> [String: Any] + """, + expandedSource: """ + func parse(json: String) throws(JSException) -> [String: Any] { + return try _$parse(json) + } + """, + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, + ) + } + + @Test func topLevelFunctionThrowsMissingType() { TestSupport.assertMacroExpansion( """ @JSFunction @@ -91,15 +134,92 @@ import BridgeJSMacros ) } + @Test func topLevelFunctionThrowsWrongType() { + TestSupport.assertMacroExpansion( + """ + @JSFunction + func parse(json: String) throws(CustomError) -> [String: Any] + """, + expandedSource: """ + func parse(json: String) throws(CustomError) -> [String: Any] { + return try _$parse(json) + } + """, + diagnostics: [ + DiagnosticSpec( + message: "@JSFunction throws must be declared as throws(JSException).", + line: 1, + column: 1, + severity: .error, + notes: [ + NoteSpec( + message: "@JSFunction must propagate JavaScript errors as JSException.", + line: 1, + column: 1 + ) + ], + fixIts: [ + FixItSpec(message: "Declare throws(JSException)") + ] + ) + ], + macroSpecs: macroSpecs, + applyFixIts: ["Declare throws(JSException)"], + fixedSource: """ + @JSFunction + func parse(json: String) throws(JSException) -> [String: Any] + """, + indentationWidth: indentationWidth, + ) + } + + @Test func topLevelFunctionMissingThrowsClause() { + TestSupport.assertMacroExpansion( + """ + @JSFunction + func greet(name: String) -> String + """, + expandedSource: """ + func greet(name: String) -> String { + return try _$greet(name) + } + """, + diagnostics: [ + DiagnosticSpec( + message: "@JSFunction throws must be declared as throws(JSException).", + line: 1, + column: 1, + notes: [ + NoteSpec( + message: "@JSFunction must propagate JavaScript errors as JSException.", + line: 1, + column: 1 + ) + ], + fixIts: [ + FixItSpec(message: "Declare throws(JSException)") + ] + ) + ], + macroSpecs: macroSpecs, + applyFixIts: ["Declare throws(JSException)"], + fixedSource: """ + @JSFunction + func greet(name: String) throws(JSException) -> String + """, + indentationWidth: indentationWidth, + ) + } + @Test func topLevelFunctionAsync() { TestSupport.assertMacroExpansion( """ @JSFunction - func fetch(url: String) async -> String + func fetch(url: String) async throws(JSException) -> String """, expandedSource: """ - func fetch(url: String) async -> String { - return await _$fetch(url) + func fetch(url: String) async throws(JSException) -> String { + return try await _$fetch(url) } """, macroSpecs: macroSpecs, @@ -111,10 +231,10 @@ import BridgeJSMacros TestSupport.assertMacroExpansion( """ @JSFunction - func fetch(url: String) async throws -> String + func fetch(url: String) async throws(JSException) -> String """, expandedSource: """ - func fetch(url: String) async throws -> String { + func fetch(url: String) async throws(JSException) -> String { return try await _$fetch(url) } """, @@ -127,11 +247,11 @@ import BridgeJSMacros TestSupport.assertMacroExpansion( """ @JSFunction - func process(_ value: Int) -> Int + func process(_ value: Int) throws(JSException) -> Int """, expandedSource: """ - func process(_ value: Int) -> Int { - return _$process(value) + func process(_ value: Int) throws(JSException) -> Int { + return try _$process(value) } """, macroSpecs: macroSpecs, @@ -143,11 +263,11 @@ import BridgeJSMacros TestSupport.assertMacroExpansion( """ @JSFunction - func add(a: Int, b: Int) -> Int + func add(a: Int, b: Int) throws(JSException) -> Int """, expandedSource: """ - func add(a: Int, b: Int) -> Int { - return _$add(a, b) + func add(a: Int, b: Int) throws(JSException) -> Int { + return try _$add(a, b) } """, macroSpecs: macroSpecs, @@ -158,15 +278,17 @@ import BridgeJSMacros @Test func instanceMethod() { TestSupport.assertMacroExpansion( """ + @JSClass struct MyClass { @JSFunction - func getName() -> String + func getName() throws(JSException) -> String } """, expandedSource: """ + @JSClass struct MyClass { - func getName() -> String { - return _$MyClass_getName(self.jsObject) + func getName() throws(JSException) -> String { + return try _$MyClass_getName(self.jsObject) } } """, @@ -180,16 +302,23 @@ import BridgeJSMacros """ struct MyClass { @JSFunction - static func create() -> MyClass + static func create() throws(JSException) -> MyClass } """, expandedSource: """ struct MyClass { - static func create() -> MyClass { - return _$MyClass_create() + static func create() throws(JSException) -> MyClass { + return try _$MyClass_create() } } """, + diagnostics: [ + DiagnosticSpec( + message: "JavaScript members must be declared inside a @JSClass struct.", + line: 2, + column: 5 + ) + ], macroSpecs: macroSpecs, indentationWidth: indentationWidth, ) @@ -200,16 +329,23 @@ import BridgeJSMacros """ class MyClass { @JSFunction - class func create() -> MyClass + class func create() throws(JSException) -> MyClass } """, expandedSource: """ class MyClass { - class func create() -> MyClass { - return _$MyClass_create() + class func create() throws(JSException) -> MyClass { + return try _$MyClass_create() } } """, + diagnostics: [ + DiagnosticSpec( + message: "JavaScript members must be declared inside a @JSClass struct.", + line: 2, + column: 5 + ) + ], macroSpecs: macroSpecs, indentationWidth: indentationWidth, ) @@ -218,15 +354,17 @@ import BridgeJSMacros @Test func initializer() { TestSupport.assertMacroExpansion( """ + @JSClass struct MyClass { @JSFunction - init(name: String) + init(name: String) throws(JSException) } """, expandedSource: """ + @JSClass struct MyClass { - init(name: String) { - let jsObject = _$MyClass_init(name) + init(name: String) throws(JSException) { + let jsObject = try _$MyClass_init(name) self.init(unsafelyWrapping: jsObject) } } @@ -239,14 +377,16 @@ import BridgeJSMacros @Test func initializerThrows() { TestSupport.assertMacroExpansion( """ + @JSClass struct MyClass { @JSFunction - init(name: String) throws + init(name: String) throws(JSException) } """, expandedSource: """ + @JSClass struct MyClass { - init(name: String) throws { + init(name: String) throws(JSException) { let jsObject = try _$MyClass_init(name) self.init(unsafelyWrapping: jsObject) } @@ -260,14 +400,16 @@ import BridgeJSMacros @Test func initializerAsyncThrows() { TestSupport.assertMacroExpansion( """ + @JSClass struct MyClass { @JSFunction - init(name: String) async throws + init(name: String) async throws(JSException) } """, expandedSource: """ + @JSClass struct MyClass { - init(name: String) async throws { + init(name: String) async throws(JSException) { let jsObject = try await _$MyClass_init(name) self.init(unsafelyWrapping: jsObject) } @@ -293,7 +435,14 @@ import BridgeJSMacros DiagnosticSpec( message: "@JSFunction can only be applied to functions or initializers.", line: 1, - column: 1 + column: 1, + notes: [ + NoteSpec( + message: "Move this initializer inside a JS wrapper type annotated with @JSClass.", + line: 1, + column: 1 + ) + ] ) ], macroSpecs: macroSpecs, @@ -314,7 +463,15 @@ import BridgeJSMacros DiagnosticSpec( message: "@JSFunction can only be applied to functions or initializers.", line: 1, - column: 1 + column: 1, + notes: [ + NoteSpec( + message: + "Place @JSFunction on a function or initializer; use @JSGetter/@JSSetter for properties.", + line: 1, + column: 1 + ) + ] ) ], macroSpecs: macroSpecs, @@ -325,15 +482,17 @@ import BridgeJSMacros @Test func enumInstanceMethod() { TestSupport.assertMacroExpansion( """ + @JSClass enum MyEnum { @JSFunction - func getValue() -> Int + func getValue() throws(JSException) -> Int } """, expandedSource: """ + @JSClass enum MyEnum { - func getValue() -> Int { - return _$MyEnum_getValue(self.jsObject) + func getValue() throws(JSException) -> Int { + return try _$MyEnum_getValue(self.jsObject) } } """, @@ -345,15 +504,17 @@ import BridgeJSMacros @Test func actorInstanceMethod() { TestSupport.assertMacroExpansion( """ + @JSClass actor MyActor { @JSFunction - func getValue() -> Int + func getValue() throws(JSException) -> Int } """, expandedSource: """ + @JSClass actor MyActor { - func getValue() -> Int { - return _$MyActor_getValue(self.jsObject) + func getValue() throws(JSException) -> Int { + return try _$MyActor_getValue(self.jsObject) } } """, @@ -366,13 +527,13 @@ import BridgeJSMacros TestSupport.assertMacroExpansion( """ @JSFunction - func greet(name: String) -> String { + func greet(name: String) throws(JSException) -> String { return "Hello, \\(name)" } """, expandedSource: """ - func greet(name: String) -> String { - return _$greet(name) + func greet(name: String) throws(JSException) -> String { + return try _$greet(name) } """, macroSpecs: macroSpecs, diff --git a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSGetterMacroTests.swift b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSGetterMacroTests.swift index 6e47475b..0e4cea84 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSGetterMacroTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSGetterMacroTests.swift @@ -50,12 +50,14 @@ import BridgeJSMacros @Test func instanceProperty() { TestSupport.assertMacroExpansion( """ + @JSClass struct MyClass { @JSGetter var name: String } """, expandedSource: """ + @JSClass struct MyClass { var name: String { get throws(JSException) { @@ -69,15 +71,46 @@ import BridgeJSMacros ) } + @Test func instancePropertyRequiresJSClass() { + TestSupport.assertMacroExpansion( + """ + struct MyClass { + @JSGetter + var name: String + } + """, + expandedSource: """ + struct MyClass { + var name: String { + get throws(JSException) { + return try _$MyClass_name_get(self.jsObject) + } + } + } + """, + diagnostics: [ + DiagnosticSpec( + message: "JavaScript members must be declared inside a @JSClass struct.", + line: 2, + column: 5 + ) + ], + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, + ) + } + @Test func instanceLetProperty() { TestSupport.assertMacroExpansion( """ + @JSClass struct MyClass { @JSGetter let id: Int } """, expandedSource: """ + @JSClass struct MyClass { let id: Int { get throws(JSException) { @@ -108,6 +141,13 @@ import BridgeJSMacros } } """, + diagnostics: [ + DiagnosticSpec( + message: "JavaScript members must be declared inside a @JSClass struct.", + line: 2, + column: 5 + ) + ], macroSpecs: macroSpecs, indentationWidth: indentationWidth, ) @@ -130,6 +170,13 @@ import BridgeJSMacros } } """, + diagnostics: [ + DiagnosticSpec( + message: "JavaScript members must be declared inside a @JSClass struct.", + line: 2, + column: 5 + ) + ], macroSpecs: macroSpecs, indentationWidth: indentationWidth, ) @@ -138,12 +185,14 @@ import BridgeJSMacros @Test func enumProperty() { TestSupport.assertMacroExpansion( """ + @JSClass enum MyEnum { @JSGetter var value: Int } """, expandedSource: """ + @JSClass enum MyEnum { var value: Int { get throws(JSException) { @@ -160,12 +209,14 @@ import BridgeJSMacros @Test func actorProperty() { TestSupport.assertMacroExpansion( """ + @JSClass actor MyActor { @JSGetter var state: String } """, expandedSource: """ + @JSClass actor MyActor { var state: String { get throws(JSException) { @@ -262,7 +313,14 @@ import BridgeJSMacros message: "@JSGetter can only be applied to single-variable declarations.", line: 1, column: 1, - severity: .error + severity: .error, + notes: [ + NoteSpec( + message: "@JSGetter must be attached to a single stored or computed property.", + line: 1, + column: 1 + ) + ] ) ], macroSpecs: macroSpecs, diff --git a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSSetterMacroTests.swift b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSSetterMacroTests.swift index 1ed4e108..00d95992 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSSetterMacroTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSSetterMacroTests.swift @@ -46,12 +46,14 @@ import BridgeJSMacros @Test func instanceSetter() { TestSupport.assertMacroExpansion( """ + @JSClass struct MyClass { @JSSetter func setName(_ name: String) throws(JSException) } """, expandedSource: """ + @JSClass struct MyClass { func setName(_ name: String) throws(JSException) { try _$MyClass_name_set(self.jsObject, name) @@ -63,6 +65,33 @@ import BridgeJSMacros ) } + @Test func instanceSetterRequiresJSClass() { + TestSupport.assertMacroExpansion( + """ + struct MyClass { + @JSSetter + func setName(_ name: String) throws(JSException) + } + """, + expandedSource: """ + struct MyClass { + func setName(_ name: String) throws(JSException) { + try _$MyClass_name_set(self.jsObject, name) + } + } + """, + diagnostics: [ + DiagnosticSpec( + message: "JavaScript members must be declared inside a @JSClass struct.", + line: 2, + column: 5 + ) + ], + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, + ) + } + @Test func staticSetter() { TestSupport.assertMacroExpansion( """ @@ -78,6 +107,13 @@ import BridgeJSMacros } } """, + diagnostics: [ + DiagnosticSpec( + message: "JavaScript members must be declared inside a @JSClass struct.", + line: 2, + column: 5 + ) + ], macroSpecs: macroSpecs, indentationWidth: indentationWidth, ) @@ -98,6 +134,13 @@ import BridgeJSMacros } } """, + diagnostics: [ + DiagnosticSpec( + message: "JavaScript members must be declared inside a @JSClass struct.", + line: 2, + column: 5 + ) + ], macroSpecs: macroSpecs, indentationWidth: indentationWidth, ) @@ -106,12 +149,14 @@ import BridgeJSMacros @Test func enumSetter() { TestSupport.assertMacroExpansion( """ + @JSClass enum MyEnum { @JSSetter func setValue(_ value: Int) throws(JSException) } """, expandedSource: """ + @JSClass enum MyEnum { func setValue(_ value: Int) throws(JSException) { try _$MyEnum_value_set(self.jsObject, value) @@ -126,12 +171,14 @@ import BridgeJSMacros @Test func actorSetter() { TestSupport.assertMacroExpansion( """ + @JSClass actor MyActor { @JSSetter func setState(_ state: String) throws(JSException) } """, expandedSource: """ + @JSClass actor MyActor { func setState(_ state: String) throws(JSException) { try _$MyActor_state_set(self.jsObject, state) @@ -177,7 +224,17 @@ import BridgeJSMacros message: "@JSSetter function name must start with 'set' followed by a property name (e.g., 'setFoo').", line: 1, - column: 1 + column: 1, + notes: [ + NoteSpec( + message: "Setter names must start with 'set' followed by the property name.", + line: 2, + column: 6 + ) + ], + fixIts: [ + FixItSpec(message: "Rename setter to 'setFoo'") + ] ) ], macroSpecs: macroSpecs, @@ -201,7 +258,17 @@ import BridgeJSMacros message: "@JSSetter function name must start with 'set' followed by a property name (e.g., 'setFoo').", line: 1, - column: 1 + column: 1, + notes: [ + NoteSpec( + message: "Setter names must start with 'set' followed by the property name.", + line: 2, + column: 6 + ) + ], + fixIts: [ + FixItSpec(message: "Rename setter to 'setFoo'") + ] ) ], macroSpecs: macroSpecs, @@ -224,10 +291,117 @@ import BridgeJSMacros DiagnosticSpec( message: "@JSSetter function must have at least one parameter.", line: 1, - column: 1 + column: 1, + notes: [ + NoteSpec( + message: "@JSSetter needs a parameter for the value being assigned.", + line: 1, + column: 1 + ) + ], + fixIts: [ + FixItSpec(message: "Add a value parameter to the setter") + ] + ) + ], + macroSpecs: macroSpecs, + applyFixIts: ["Add a value parameter to the setter"], + fixedSource: """ + @JSSetter + func setFoo(_ value: <#Type#>) throws(JSException) + """, + indentationWidth: indentationWidth, + ) + } + + @Test func setterMissingThrows() { + TestSupport.assertMacroExpansion( + """ + @JSSetter + func setFoo(_ value: Foo) + """, + expandedSource: """ + func setFoo(_ value: Foo) { + try _$foo_set(value) + } + """, + diagnostics: [ + DiagnosticSpec( + message: "@JSSetter function must declare throws(JSException).", + line: 1, + column: 1, + notes: [ + NoteSpec( + message: "@JSSetter must propagate JavaScript errors as JSException.", + line: 1, + column: 1 + ) + ], + fixIts: [ + FixItSpec(message: "Declare throws(JSException)") + ] ) ], macroSpecs: macroSpecs, + applyFixIts: ["Declare throws(JSException)"], + fixedSource: """ + @JSSetter + func setFoo(_ value: Foo) throws(JSException) + """, + indentationWidth: indentationWidth, + ) + } + + @Test func setterThrowsWrongType() { + TestSupport.assertMacroExpansion( + """ + @JSSetter + func setFoo(_ value: Foo) throws(CustomError) + """, + expandedSource: """ + func setFoo(_ value: Foo) throws(CustomError) { + try _$foo_set(value) + } + """, + diagnostics: [ + DiagnosticSpec( + message: "@JSSetter function must declare throws(JSException).", + line: 1, + column: 1, + notes: [ + NoteSpec( + message: "@JSSetter must propagate JavaScript errors as JSException.", + line: 1, + column: 1 + ) + ], + fixIts: [ + FixItSpec(message: "Declare throws(JSException)") + ] + ) + ], + macroSpecs: macroSpecs, + applyFixIts: ["Declare throws(JSException)"], + fixedSource: """ + @JSSetter + func setFoo(_ value: Foo) throws(JSException) + """, + indentationWidth: indentationWidth, + ) + } + + @Test func setterThrowsErrorAccepted() { + TestSupport.assertMacroExpansion( + """ + @JSSetter + func setFoo(_ value: Foo) throws(Error) + """, + expandedSource: """ + func setFoo(_ value: Foo) throws(Error) { + try _$foo_set(value) + } + """, + macroSpecs: macroSpecs, indentationWidth: indentationWidth, ) } @@ -245,7 +419,14 @@ import BridgeJSMacros DiagnosticSpec( message: "@JSSetter can only be applied to functions.", line: 1, - column: 1 + column: 1, + notes: [ + NoteSpec( + message: "@JSSetter should be attached to a method that writes a JavaScript property.", + line: 1, + column: 1 + ) + ] ) ], macroSpecs: macroSpecs, diff --git a/Sources/JavaScriptKit/Macros.swift b/Sources/JavaScriptKit/Macros.swift index 329945f2..8decf1a7 100644 --- a/Sources/JavaScriptKit/Macros.swift +++ b/Sources/JavaScriptKit/Macros.swift @@ -202,7 +202,7 @@ public macro JSFunction(jsName: String? = nil, from: JSImportFrom? = nil) = /// /// - Parameter from: Selects where the constructor is looked up from. /// Use `.global` to construct globals like `WebSocket` via `globalThis`. -@attached(member, names: arbitrary) +@attached(member, names: named(jsObject), named(init(unsafelyWrapping:))) @attached(extension, conformances: _JSBridgedClass) @_spi(Experimental) public macro JSClass(jsName: String? = nil, from: JSImportFrom? = nil) =