Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Plugins/BridgeJS/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ let package = Package(
name: "BridgeJSMacrosTests",
dependencies: [
"BridgeJSMacros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacrosGenericTestSupport", package: "swift-syntax"),
]
),

Expand Down
9 changes: 8 additions & 1 deletion Plugins/BridgeJS/Sources/BridgeJSMacros/JSClassMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}")
Expand Down
18 changes: 17 additions & 1 deletion Plugins/BridgeJS/Sources/BridgeJSMacros/JSFunctionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 []
}
}
17 changes: 17 additions & 0 deletions Plugins/BridgeJS/Sources/BridgeJSMacros/JSGetterMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
}
}
38 changes: 35 additions & 3 deletions Plugins/BridgeJS/Sources/BridgeJSMacros/JSSetterMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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 []
}
}
54 changes: 28 additions & 26 deletions Plugins/BridgeJS/Tests/BridgeJSMacrosTests/JSClassMacroTests.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxMacroExpansion
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import SwiftSyntaxMacrosGenericTestSupport
import Testing
import BridgeJSMacros

Expand All @@ -13,14 +12,15 @@ import BridgeJSMacros
]

@Test func emptyStruct() {
assertMacroExpansion(
TestSupport.assertMacroExpansion(
"""
@JSClass
struct MyClass {
}
""",
expandedSource: """
struct MyClass {

let jsObject: JSObject

init(unsafelyWrapping jsObject: JSObject) {
Expand All @@ -32,12 +32,12 @@ import BridgeJSMacros
}
""",
macroSpecs: macroSpecs,
indentationWidth: indentationWidth
indentationWidth: indentationWidth,
)
}

@Test func structWithExistingJSObject() {
assertMacroExpansion(
TestSupport.assertMacroExpansion(
"""
@JSClass
struct MyClass {
Expand All @@ -62,7 +62,7 @@ import BridgeJSMacros
}

@Test func structWithExistingInit() {
assertMacroExpansion(
TestSupport.assertMacroExpansion(
"""
@JSClass
struct MyClass {
Expand All @@ -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 {
Expand All @@ -89,7 +89,7 @@ import BridgeJSMacros
}

@Test func structWithBothExisting() {
assertMacroExpansion(
TestSupport.assertMacroExpansion(
"""
@JSClass
struct MyClass {
Expand Down Expand Up @@ -118,7 +118,7 @@ import BridgeJSMacros
}

@Test func structWithMembers() {
assertMacroExpansion(
TestSupport.assertMacroExpansion(
"""
@JSClass
struct MyClass {
Expand All @@ -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
}
Expand All @@ -145,7 +145,7 @@ import BridgeJSMacros
}

@Test func _class() {
assertMacroExpansion(
TestSupport.assertMacroExpansion(
"""
@JSClass
class MyClass {
Expand All @@ -168,7 +168,7 @@ import BridgeJSMacros
}

@Test func _enum() {
assertMacroExpansion(
TestSupport.assertMacroExpansion(
"""
@JSClass
enum MyEnum {
Expand All @@ -191,7 +191,7 @@ import BridgeJSMacros
}

@Test func _actor() {
assertMacroExpansion(
TestSupport.assertMacroExpansion(
"""
@JSClass
actor MyActor {
Expand All @@ -214,7 +214,7 @@ import BridgeJSMacros
}

@Test func structWithDifferentJSObjectName() {
assertMacroExpansion(
TestSupport.assertMacroExpansion(
"""
@JSClass
struct MyClass {
Expand All @@ -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
}
Expand All @@ -241,7 +241,7 @@ import BridgeJSMacros
}

@Test func structWithDifferentInit() {
assertMacroExpansion(
TestSupport.assertMacroExpansion(
"""
@JSClass
struct MyClass {
Expand All @@ -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
}
Expand All @@ -270,7 +270,7 @@ import BridgeJSMacros
}

@Test func structWithMultipleMembers() {
assertMacroExpansion(
TestSupport.assertMacroExpansion(
"""
@JSClass
struct MyClass {
Expand All @@ -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
}
Expand All @@ -299,7 +299,7 @@ import BridgeJSMacros
}

@Test func structWithComment() {
assertMacroExpansion(
TestSupport.assertMacroExpansion(
"""
/// Documentation comment
@JSClass
Expand All @@ -309,6 +309,7 @@ import BridgeJSMacros
expandedSource: """
/// Documentation comment
struct MyClass {

let jsObject: JSObject

init(unsafelyWrapping jsObject: JSObject) {
Expand All @@ -325,14 +326,15 @@ import BridgeJSMacros
}

@Test func structAlreadyConforms() {
assertMacroExpansion(
TestSupport.assertMacroExpansion(
"""
@JSClass
struct MyClass: _JSBridgedClass {
}
""",
expandedSource: """
struct MyClass: _JSBridgedClass {

let jsObject: JSObject

init(unsafelyWrapping jsObject: JSObject) {
Expand Down
Loading