diff --git a/Plugins/BridgeJS/Package.swift b/Plugins/BridgeJS/Package.swift index 01a3c6d4..a03172f1 100644 --- a/Plugins/BridgeJS/Package.swift +++ b/Plugins/BridgeJS/Package.swift @@ -72,7 +72,7 @@ let package = Package( name: "BridgeJSMacrosTests", dependencies: [ "BridgeJSMacros", - .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + .product(name: "SwiftSyntaxMacrosGenericTestSupport", package: "swift-syntax"), ] ), diff --git a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift index 8a3f83f0..2641df4b 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift @@ -66,9 +66,16 @@ extension JSClassMacro: ExtensionMacro { conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext ) throws -> [ExtensionDeclSyntax] { - guard declaration.is(StructDeclSyntax.self) else { return [] } + guard let structDecl = declaration.as(StructDeclSyntax.self) else { return [] } guard !protocols.isEmpty else { return [] } + // Do not add extension if the struct already conforms to _JSBridgedClass + if let clause = structDecl.inheritanceClause, + clause.inheritedTypes.contains(where: { $0.type.trimmed.description == "_JSBridgedClass" }) + { + return [] + } + let conformanceList = protocols.map { $0.trimmed.description }.joined(separator: ", ") return [ try ExtensionDeclSyntax("extension \(type.trimmed): \(raw: conformanceList) {}") diff --git a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSFunctionMacro.swift b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSFunctionMacro.swift index 7dfd635f..a51bf10c 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSFunctionMacro.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSFunctionMacro.swift @@ -47,7 +47,7 @@ extension JSFunctionMacro: BodyMacro { context.diagnose( Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedDeclaration) ) - return [] + return [CodeBlockItemSyntax(stringLiteral: "fatalError(\"@JSFunction init must be inside a type\")")] } let glueName = JSMacroHelper.glueName(baseName: "init", enclosingTypeName: enclosingTypeName) @@ -70,3 +70,19 @@ extension JSFunctionMacro: BodyMacro { return [] } } + +extension JSFunctionMacro: PeerMacro { + /// Emits a diagnostic when @JSFunction is applied to a declaration that is not a function or initializer. + /// BodyMacro is only invoked for declarations with optional code blocks (e.g. functions, initializers), + /// so for vars and other decls we need PeerMacro to run and diagnose. + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + if declaration.is(FunctionDeclSyntax.self) { return [] } + if declaration.is(InitializerDeclSyntax.self) { return [] } + context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedDeclaration)) + return [] + } +} diff --git a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSGetterMacro.swift b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSGetterMacro.swift index b996facf..44c3620c 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSGetterMacro.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSGetterMacro.swift @@ -60,3 +60,20 @@ extension JSGetterMacro: AccessorMacro { ] } } + +extension JSGetterMacro: PeerMacro { + /// Emits a diagnostic when @JSGetter is applied to a declaration that is not a variable (e.g. a function). + /// AccessorMacro may not be invoked for non-property declarations. For variables with multiple + /// bindings, the compiler emits its own diagnostic; we only diagnose non-variable decls here. + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + guard declaration.is(VariableDeclSyntax.self) else { + context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedVariable)) + return [] + } + return [] + } +} diff --git a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSSetterMacro.swift b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSSetterMacro.swift index bb9fd1f2..3c728007 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSMacros/JSSetterMacro.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSMacros/JSSetterMacro.swift @@ -25,13 +25,23 @@ extension JSSetterMacro: BodyMacro { let rawFunctionName = JSMacroHelper.stripBackticks(functionName) guard rawFunctionName.hasPrefix("set"), rawFunctionName.count > 3 else { context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.invalidSetterName)) - return [] + return [ + CodeBlockItemSyntax( + stringLiteral: + "fatalError(\"@JSSetter function name must start with 'set' followed by a property name\")" + ) + ] } let propertyName = String(rawFunctionName.dropFirst(3)) guard !propertyName.isEmpty else { context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.invalidSetterName)) - return [] + return [ + CodeBlockItemSyntax( + stringLiteral: + "fatalError(\"@JSSetter function name must start with 'set' followed by a property name\")" + ) + ] } // Convert first character to lowercase (e.g., "Foo" -> "foo") @@ -56,7 +66,11 @@ extension JSSetterMacro: BodyMacro { let parameters = functionDecl.signature.parameterClause.parameters guard let firstParam = parameters.first else { context.diagnose(Diagnostic(node: Syntax(declaration), message: JSMacroMessage.setterRequiresParameter)) - return [] + return [ + CodeBlockItemSyntax( + stringLiteral: "fatalError(\"@JSSetter function must have at least one parameter\")" + ) + ] } let paramName = firstParam.secondName ?? firstParam.firstName @@ -69,3 +83,21 @@ extension JSSetterMacro: BodyMacro { return [CodeBlockItemSyntax(stringLiteral: "try \(call)")] } } + +extension JSSetterMacro: PeerMacro { + /// Emits a diagnostic when @JSSetter is applied to a declaration that is not a function. + /// BodyMacro is only invoked for declarations with optional code blocks (e.g. functions). + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + guard declaration.is(FunctionDeclSyntax.self) else { + context.diagnose( + Diagnostic(node: Syntax(declaration), message: JSMacroMessage.unsupportedSetterDeclaration) + ) + return [] + } + return [] + } +} diff --git a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift index b27b9394..7640916e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift @@ -1,8 +1,7 @@ import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacroExpansion -import SwiftSyntaxMacros -import SwiftSyntaxMacrosTestSupport +import SwiftSyntaxMacrosGenericTestSupport import Testing import BridgeJSMacros @@ -13,7 +12,7 @@ import BridgeJSMacros ] @Test func emptyStruct() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSClass struct MyClass { @@ -21,6 +20,7 @@ import BridgeJSMacros """, expandedSource: """ struct MyClass { + let jsObject: JSObject init(unsafelyWrapping jsObject: JSObject) { @@ -32,12 +32,12 @@ import BridgeJSMacros } """, macroSpecs: macroSpecs, - indentationWidth: indentationWidth + indentationWidth: indentationWidth, ) } @Test func structWithExistingJSObject() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSClass struct MyClass { @@ -62,7 +62,7 @@ import BridgeJSMacros } @Test func structWithExistingInit() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSClass struct MyClass { @@ -73,11 +73,11 @@ import BridgeJSMacros """, expandedSource: """ struct MyClass { - let jsObject: JSObject - init(unsafelyWrapping jsObject: JSObject) { self.jsObject = jsObject } + + let jsObject: JSObject } extension MyClass: _JSBridgedClass { @@ -89,7 +89,7 @@ import BridgeJSMacros } @Test func structWithBothExisting() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSClass struct MyClass { @@ -118,7 +118,7 @@ import BridgeJSMacros } @Test func structWithMembers() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSClass struct MyClass { @@ -127,10 +127,10 @@ import BridgeJSMacros """, expandedSource: """ struct MyClass { - let jsObject: JSObject - var name: String + let jsObject: JSObject + init(unsafelyWrapping jsObject: JSObject) { self.jsObject = jsObject } @@ -145,7 +145,7 @@ import BridgeJSMacros } @Test func _class() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSClass class MyClass { @@ -168,7 +168,7 @@ import BridgeJSMacros } @Test func _enum() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSClass enum MyEnum { @@ -191,7 +191,7 @@ import BridgeJSMacros } @Test func _actor() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSClass actor MyActor { @@ -214,7 +214,7 @@ import BridgeJSMacros } @Test func structWithDifferentJSObjectName() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSClass struct MyClass { @@ -223,10 +223,10 @@ import BridgeJSMacros """, expandedSource: """ struct MyClass { - let jsObject: JSObject - var otherProperty: String + let jsObject: JSObject + init(unsafelyWrapping jsObject: JSObject) { self.jsObject = jsObject } @@ -241,7 +241,7 @@ import BridgeJSMacros } @Test func structWithDifferentInit() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSClass struct MyClass { @@ -251,11 +251,11 @@ import BridgeJSMacros """, expandedSource: """ struct MyClass { - let jsObject: JSObject - init(name: String) { } + let jsObject: JSObject + init(unsafelyWrapping jsObject: JSObject) { self.jsObject = jsObject } @@ -270,7 +270,7 @@ import BridgeJSMacros } @Test func structWithMultipleMembers() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSClass struct MyClass { @@ -280,11 +280,11 @@ import BridgeJSMacros """, expandedSource: """ struct MyClass { - let jsObject: JSObject - var name: String var age: Int + let jsObject: JSObject + init(unsafelyWrapping jsObject: JSObject) { self.jsObject = jsObject } @@ -299,7 +299,7 @@ import BridgeJSMacros } @Test func structWithComment() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ /// Documentation comment @JSClass @@ -309,6 +309,7 @@ import BridgeJSMacros expandedSource: """ /// Documentation comment struct MyClass { + let jsObject: JSObject init(unsafelyWrapping jsObject: JSObject) { @@ -325,7 +326,7 @@ import BridgeJSMacros } @Test func structAlreadyConforms() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSClass struct MyClass: _JSBridgedClass { @@ -333,6 +334,7 @@ import BridgeJSMacros """, expandedSource: """ struct MyClass: _JSBridgedClass { + let jsObject: JSObject init(unsafelyWrapping jsObject: JSObject) { diff --git a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSFunctionMacroTests.swift b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSFunctionMacroTests.swift index 8cfdd66e..756935e7 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSFunctionMacroTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSFunctionMacroTests.swift @@ -1,15 +1,18 @@ import SwiftDiagnostics import SwiftSyntax -import SwiftSyntaxMacros -import SwiftSyntaxMacrosTestSupport +import SwiftSyntaxMacroExpansion +import SwiftSyntaxMacrosGenericTestSupport import Testing import BridgeJSMacros @Suite struct JSFunctionMacroTests { private let indentationWidth: Trivia = .spaces(4) + private let macroSpecs: [String: MacroSpec] = [ + "JSFunction": MacroSpec(type: JSFunctionMacro.self) + ] @Test func topLevelFunction() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSFunction func greet(name: String) -> String @@ -19,13 +22,13 @@ import BridgeJSMacros return _$greet(name) } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func topLevelFunctionVoidReturn() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSFunction func log(message: String) @@ -35,13 +38,13 @@ import BridgeJSMacros _$log(message) } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func topLevelFunctionWithExplicitVoidReturn() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSFunction func log(message: String) -> Void @@ -51,13 +54,13 @@ import BridgeJSMacros _$log(message) } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func topLevelFunctionWithEmptyTupleReturn() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSFunction func log(message: String) -> () @@ -67,13 +70,13 @@ import BridgeJSMacros _$log(message) } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func topLevelFunctionThrows() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSFunction func parse(json: String) throws -> [String: Any] @@ -83,13 +86,13 @@ import BridgeJSMacros return try _$parse(json) } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func topLevelFunctionAsync() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSFunction func fetch(url: String) async -> String @@ -99,13 +102,13 @@ import BridgeJSMacros return await _$fetch(url) } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func topLevelFunctionAsyncThrows() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSFunction func fetch(url: String) async throws -> String @@ -115,13 +118,13 @@ import BridgeJSMacros return try await _$fetch(url) } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func topLevelFunctionWithUnderscoreParameter() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSFunction func process(_ value: Int) -> Int @@ -131,13 +134,13 @@ import BridgeJSMacros return _$process(value) } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func topLevelFunctionWithMultipleParameters() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSFunction func add(a: Int, b: Int) -> Int @@ -147,13 +150,13 @@ import BridgeJSMacros return _$add(a, b) } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func instanceMethod() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ struct MyClass { @JSFunction @@ -167,13 +170,13 @@ import BridgeJSMacros } } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func staticMethod() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ struct MyClass { @JSFunction @@ -183,17 +186,17 @@ import BridgeJSMacros expandedSource: """ struct MyClass { static func create() -> MyClass { - return _$create() + return _$MyClass_create() } } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func classMethod() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ class MyClass { @JSFunction @@ -203,17 +206,17 @@ import BridgeJSMacros expandedSource: """ class MyClass { class func create() -> MyClass { - return _$create() + return _$MyClass_create() } } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func initializer() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ struct MyClass { @JSFunction @@ -228,13 +231,13 @@ import BridgeJSMacros } } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func initializerThrows() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ struct MyClass { @JSFunction @@ -249,13 +252,13 @@ import BridgeJSMacros } } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func initializerAsyncThrows() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ struct MyClass { @JSFunction @@ -270,19 +273,21 @@ import BridgeJSMacros } } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func initializerWithoutEnclosingType() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSFunction init() """, expandedSource: """ - init() + init() { + fatalError("@JSFunction init must be inside a type") + } """, diagnostics: [ DiagnosticSpec( @@ -291,13 +296,13 @@ import BridgeJSMacros column: 1 ) ], - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func unsupportedDeclaration() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSFunction var property: String @@ -312,13 +317,13 @@ import BridgeJSMacros column: 1 ) ], - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func enumInstanceMethod() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ enum MyEnum { @JSFunction @@ -332,13 +337,13 @@ import BridgeJSMacros } } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func actorInstanceMethod() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ actor MyActor { @JSFunction @@ -352,13 +357,13 @@ import BridgeJSMacros } } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func functionWithExistingBody() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSFunction func greet(name: String) -> String { @@ -370,8 +375,8 @@ import BridgeJSMacros return _$greet(name) } """, - macros: ["JSFunction": JSFunctionMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSGetterMacroTests.swift b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSGetterMacroTests.swift index 8c7f3c11..2796c101 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSGetterMacroTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSGetterMacroTests.swift @@ -1,15 +1,18 @@ import SwiftDiagnostics import SwiftSyntax -import SwiftSyntaxMacros -import SwiftSyntaxMacrosTestSupport +import SwiftSyntaxMacroExpansion +import SwiftSyntaxMacrosGenericTestSupport import Testing import BridgeJSMacros @Suite struct JSGetterMacroTests { private let indentationWidth: Trivia = .spaces(4) + private let macroSpecs: [String: MacroSpec] = [ + "JSGetter": MacroSpec(type: JSGetterMacro.self) + ] @Test func topLevelVariable() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSGetter var count: Int @@ -21,13 +24,13 @@ import BridgeJSMacros } } """, - macros: ["JSGetter": JSGetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func topLevelLet() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSGetter let constant: String @@ -39,13 +42,13 @@ import BridgeJSMacros } } """, - macros: ["JSGetter": JSGetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func instanceProperty() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ struct MyClass { @JSGetter @@ -61,13 +64,13 @@ import BridgeJSMacros } } """, - macros: ["JSGetter": JSGetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func instanceLetProperty() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ struct MyClass { @JSGetter @@ -83,13 +86,13 @@ import BridgeJSMacros } } """, - macros: ["JSGetter": JSGetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func staticProperty() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ struct MyClass { @JSGetter @@ -100,18 +103,18 @@ import BridgeJSMacros struct MyClass { static var version: String { get throws(JSException) { - return try _$version_get() + return try _$MyClass_version_get() } } } """, - macros: ["JSGetter": JSGetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func classProperty() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ class MyClass { @JSGetter @@ -122,18 +125,18 @@ import BridgeJSMacros class MyClass { class var version: String { get throws(JSException) { - return try _$version_get() + return try _$MyClass_version_get() } } } """, - macros: ["JSGetter": JSGetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func enumProperty() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ enum MyEnum { @JSGetter @@ -149,13 +152,13 @@ import BridgeJSMacros } } """, - macros: ["JSGetter": JSGetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func actorProperty() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ actor MyActor { @JSGetter @@ -171,13 +174,13 @@ import BridgeJSMacros } } """, - macros: ["JSGetter": JSGetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func variableWithExistingAccessor() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSGetter var count: Int { @@ -194,13 +197,13 @@ import BridgeJSMacros } } """, - macros: ["JSGetter": JSGetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func variableWithInitializer() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSGetter var count: Int = 0 @@ -212,13 +215,13 @@ import BridgeJSMacros } } """, - macros: ["JSGetter": JSGetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func multipleBindings() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSGetter var x: Int, y: Int @@ -228,19 +231,25 @@ import BridgeJSMacros """, diagnostics: [ DiagnosticSpec( - message: "@JSGetter can only be applied to single-variable declarations.", + message: "accessor macro can only be applied to a single variable", line: 1, column: 1, severity: .error - ) + ), + DiagnosticSpec( + message: "peer macro can only be applied to a single variable", + line: 1, + column: 1, + severity: .error + ), ], - macros: ["JSGetter": JSGetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func unsupportedDeclaration() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSGetter func test() {} @@ -256,13 +265,15 @@ import BridgeJSMacros severity: .error ) ], - macros: ["JSGetter": JSGetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } + #if canImport(SwiftSyntax601) + // https://github.com/swiftlang/swift-syntax/pull/2722 @Test func variableWithTrailingComment() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSGetter var count: Int // comment @@ -274,13 +285,14 @@ import BridgeJSMacros } } """, - macros: ["JSGetter": JSGetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } + #endif @Test func variableWithUnderscoreName() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSGetter var _internal: String @@ -288,12 +300,12 @@ import BridgeJSMacros expandedSource: """ var _internal: String { get throws(JSException) { - return try _$internal_get() + return try _$_internal_get() } } """, - macros: ["JSGetter": JSGetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSSetterMacroTests.swift b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSSetterMacroTests.swift index 83289e09..1ed4e108 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSSetterMacroTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSSetterMacroTests.swift @@ -1,15 +1,18 @@ import SwiftDiagnostics import SwiftSyntax -import SwiftSyntaxMacros -import SwiftSyntaxMacrosTestSupport +import SwiftSyntaxMacroExpansion +import SwiftSyntaxMacrosGenericTestSupport import Testing import BridgeJSMacros @Suite struct JSSetterMacroTests { private let indentationWidth: Trivia = .spaces(4) + private let macroSpecs: [String: MacroSpec] = [ + "JSSetter": MacroSpec(type: JSSetterMacro.self) + ] @Test func topLevelSetter() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSSetter func setFoo(_ value: Foo) throws(JSException) @@ -19,13 +22,13 @@ import BridgeJSMacros try _$foo_set(value) } """, - macros: ["JSSetter": JSSetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func topLevelSetterWithNamedParameter() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSSetter func setCount(count: Int) throws(JSException) @@ -35,13 +38,13 @@ import BridgeJSMacros try _$count_set(count) } """, - macros: ["JSSetter": JSSetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func instanceSetter() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ struct MyClass { @JSSetter @@ -55,13 +58,13 @@ import BridgeJSMacros } } """, - macros: ["JSSetter": JSSetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func staticSetter() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ struct MyClass { @JSSetter @@ -71,17 +74,17 @@ import BridgeJSMacros expandedSource: """ struct MyClass { static func setVersion(_ version: String) throws(JSException) { - try _$version_set(version) + try _$MyClass_version_set(version) } } """, - macros: ["JSSetter": JSSetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func classSetter() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ class MyClass { @JSSetter @@ -91,17 +94,17 @@ import BridgeJSMacros expandedSource: """ class MyClass { class func setConfig(_ config: Config) throws(JSException) { - try _$config_set(config) + try _$MyClass_config_set(config) } } """, - macros: ["JSSetter": JSSetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func enumSetter() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ enum MyEnum { @JSSetter @@ -115,13 +118,13 @@ import BridgeJSMacros } } """, - macros: ["JSSetter": JSSetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func actorSetter() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ actor MyActor { @JSSetter @@ -135,13 +138,13 @@ import BridgeJSMacros } } """, - macros: ["JSSetter": JSSetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func setterWithExistingBody() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSSetter func setFoo(_ value: Foo) throws(JSException) { @@ -153,19 +156,21 @@ import BridgeJSMacros try _$foo_set(value) } """, - macros: ["JSSetter": JSSetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func invalidSetterName() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSSetter func updateFoo(_ value: Foo) throws(JSException) """, expandedSource: """ - func updateFoo(_ value: Foo) throws(JSException) + func updateFoo(_ value: Foo) throws(JSException) { + fatalError("@JSSetter function name must start with 'set' followed by a property name") + } """, diagnostics: [ DiagnosticSpec( @@ -175,19 +180,21 @@ import BridgeJSMacros column: 1 ) ], - macros: ["JSSetter": JSSetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func setterNameTooShort() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSSetter func set(_ value: Foo) throws(JSException) """, expandedSource: """ - func set(_ value: Foo) throws(JSException) + func set(_ value: Foo) throws(JSException) { + fatalError("@JSSetter function name must start with 'set' followed by a property name") + } """, diagnostics: [ DiagnosticSpec( @@ -197,19 +204,21 @@ import BridgeJSMacros column: 1 ) ], - macros: ["JSSetter": JSSetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func setterWithoutParameter() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSSetter func setFoo() throws(JSException) """, expandedSource: """ - func setFoo() throws(JSException) + func setFoo() throws(JSException) { + fatalError("@JSSetter function must have at least one parameter") + } """, diagnostics: [ DiagnosticSpec( @@ -218,13 +227,13 @@ import BridgeJSMacros column: 1 ) ], - macros: ["JSSetter": JSSetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func unsupportedDeclaration() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSSetter var property: String @@ -239,13 +248,13 @@ import BridgeJSMacros column: 1 ) ], - macros: ["JSSetter": JSSetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func setterWithMultipleWords() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSSetter func setConnectionTimeout(_ timeout: Int) throws(JSException) @@ -255,13 +264,13 @@ import BridgeJSMacros try _$connectionTimeout_set(timeout) } """, - macros: ["JSSetter": JSSetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } @Test func setterWithSingleLetterProperty() { - assertMacroExpansion( + TestSupport.assertMacroExpansion( """ @JSSetter func setX(_ x: Int) throws(JSException) @@ -271,8 +280,8 @@ import BridgeJSMacros try _$x_set(x) } """, - macros: ["JSSetter": JSSetterMacro.self], - indentationWidth: indentationWidth + macroSpecs: macroSpecs, + indentationWidth: indentationWidth, ) } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/TestSupport.swift b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/TestSupport.swift new file mode 100644 index 00000000..84e34328 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSMacrosTests/TestSupport.swift @@ -0,0 +1,52 @@ +import SwiftSyntaxMacrosGenericTestSupport +import Testing +import SwiftSyntax +import SwiftDiagnostics +import SwiftSyntaxMacroExpansion + +enum TestSupport { + static func failureHandler(spec: TestFailureSpec) { + Issue.record( + Comment(rawValue: spec.message), + sourceLocation: .init( + fileID: spec.location.fileID, + filePath: spec.location.filePath, + line: spec.location.line, + column: spec.location.column + ) + ) + } + + static func assertMacroExpansion( + _ originalSource: String, + expandedSource expectedExpandedSource: String, + diagnostics: [DiagnosticSpec] = [], + macroSpecs: [String: MacroSpec], + applyFixIts: [String]? = nil, + fixedSource expectedFixedSource: String? = nil, + testModuleName: String = "TestModule", + testFileName: String = "test.swift", + indentationWidth: Trivia = .spaces(4), + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column + ) { + SwiftSyntaxMacrosGenericTestSupport.assertMacroExpansion( + originalSource, + expandedSource: expectedExpandedSource, + diagnostics: diagnostics, + macroSpecs: macroSpecs, + applyFixIts: applyFixIts, + fixedSource: expectedFixedSource, + testModuleName: testModuleName, + testFileName: testFileName, + indentationWidth: indentationWidth, + failureHandler: TestSupport.failureHandler, + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) + } +}