Skip to content

Commit cc57cc4

Browse files
- Static @JSFunction support now round-trips through skeletons with a dedicated staticMethods collection and backward-compatible encoding/decoding (Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift).
- Import collector keeps JSClass static functions attached to their types (including extensions), avoids type-name prefixing for class-bound statics, and preserves existing global behavior for non-JSClass cases (`Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift`). - Import thunks and JS glue generate distinct `_static` ABI symbols and invoke constructor properties instead of global lookups for static methods, preventing collisions with instance methods (`Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift`, `Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift`). - Added a macro fixture covering static JS functions (including same-name instance/static and dashed jsName) with new snapshots to guard behavior (`Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/JSClassStaticFunctions.swift`, corresponding snapshot files under `Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/`). Tests: - `swift test --package-path ./Plugins/BridgeJS --filter BridgeJSCodegenTests` Next steps: 1) Run `make unittest SWIFT_SDK_ID=DEVELOPMENT-SNAPSHOT-2025-11-03-a-wasm32-unknown-wasip1` for full coverage.
1 parent edabb97 commit cc57cc4

File tree

7 files changed

+365
-8
lines changed

7 files changed

+365
-8
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,24 @@ public struct ImportTS {
398398
]
399399
}
400400

401+
func renderStaticMethod(method: ImportedFunctionSkeleton) throws -> [DeclSyntax] {
402+
let abiName = method.abiName(context: type, operation: "static")
403+
let builder = CallJSEmission(moduleName: moduleName, abiName: abiName)
404+
for param in method.parameters {
405+
try builder.lowerParameter(param: param)
406+
}
407+
try builder.call(returnType: method.returnType)
408+
try builder.liftReturnValue(returnType: method.returnType)
409+
topLevelDecls.append(builder.renderImportDecl())
410+
return [
411+
builder.renderThunkDecl(
412+
name: Self.thunkName(type: type, method: method),
413+
parameters: method.parameters,
414+
returnType: method.returnType
415+
)
416+
]
417+
}
418+
401419
func renderConstructorDecl(constructor: ImportedConstructorSkeleton) throws -> [DeclSyntax] {
402420
let builder = CallJSEmission(moduleName: moduleName, abiName: constructor.abiName(context: type))
403421
for param in constructor.parameters {
@@ -462,6 +480,10 @@ public struct ImportTS {
462480
decls.append(contentsOf: try renderConstructorDecl(constructor: constructor))
463481
}
464482

483+
for method in type.staticMethods {
484+
decls.append(contentsOf: try renderStaticMethod(method: method))
485+
}
486+
465487
for getter in type.getters {
466488
decls.append(try renderGetterDecl(getter: getter))
467489
}

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,6 +1856,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
18561856
private let inputFilePath: String
18571857
private var jsClassNames: Set<String>
18581858
private let parent: SwiftToSkeleton
1859+
private var staticMethodsByType: [String: [ImportedFunctionSkeleton]] = [:]
18591860

18601861
// MARK: - State Management
18611862

@@ -1876,6 +1877,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
18761877
let from: JSImportFrom?
18771878
var constructor: ImportedConstructorSkeleton?
18781879
var methods: [ImportedFunctionSkeleton]
1880+
var staticMethods: [ImportedFunctionSkeleton]
18791881
var getters: [ImportedGetterSkeleton]
18801882
var setters: [ImportedSetterSkeleton]
18811883
}
@@ -2094,6 +2096,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
20942096
from: nil,
20952097
constructor: nil,
20962098
methods: [],
2099+
staticMethods: [],
20972100
getters: [],
20982101
setters: []
20992102
)
@@ -2107,25 +2110,29 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
21072110
from: from,
21082111
constructor: nil,
21092112
methods: [],
2113+
staticMethods: [],
21102114
getters: [],
21112115
setters: []
21122116
)
21132117
}
21142118

21152119
private func exitJSClass() {
21162120
if case .jsClassBody(let typeName) = state, let type = currentType, type.name == typeName {
2121+
let externalStaticMethods = staticMethodsByType[type.name] ?? []
21172122
importedTypes.append(
21182123
ImportedTypeSkeleton(
21192124
name: type.name,
21202125
jsName: type.jsName,
21212126
from: type.from,
21222127
constructor: type.constructor,
21232128
methods: type.methods,
2129+
staticMethods: type.staticMethods + externalStaticMethods,
21242130
getters: type.getters,
21252131
setters: type.setters,
21262132
documentation: nil
21272133
)
21282134
)
2135+
staticMethodsByType[type.name] = nil
21292136
currentType = nil
21302137
}
21312138
stateStack.removeLast()
@@ -2217,8 +2224,14 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
22172224
) -> Bool {
22182225
if let jsFunction = AttributeChecker.firstJSFunctionAttribute(node.attributes) {
22192226
if isStaticMember {
2220-
parseFunction(jsFunction, node, enclosingTypeName: typeName, isStaticMember: true).map {
2221-
importedFunctions.append($0)
2227+
parseFunction(
2228+
jsFunction,
2229+
node,
2230+
enclosingTypeName: typeName,
2231+
isStaticMember: true,
2232+
includeTypeNameForStatic: false
2233+
).map {
2234+
type.staticMethods.append($0)
22222235
}
22232236
} else {
22242237
parseFunction(jsFunction, node, enclosingTypeName: typeName, isStaticMember: false).map {
@@ -2319,9 +2332,34 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
23192332
for member in members {
23202333
if let function = member.decl.as(FunctionDeclSyntax.self) {
23212334
if let jsFunction = AttributeChecker.firstJSFunctionAttribute(function.attributes),
2322-
let parsed = parseFunction(jsFunction, function, enclosingTypeName: typeName, isStaticMember: true)
2335+
let parsed = parseFunction(
2336+
jsFunction,
2337+
function,
2338+
enclosingTypeName: typeName,
2339+
isStaticMember: true,
2340+
includeTypeNameForStatic: !jsClassNames.contains(typeName)
2341+
)
23232342
{
2324-
importedFunctions.append(parsed)
2343+
if jsClassNames.contains(typeName) {
2344+
if let index = importedTypes.firstIndex(where: { $0.name == typeName }) {
2345+
let existing = importedTypes[index]
2346+
importedTypes[index] = ImportedTypeSkeleton(
2347+
name: existing.name,
2348+
jsName: existing.jsName,
2349+
from: existing.from,
2350+
constructor: existing.constructor,
2351+
methods: existing.methods,
2352+
staticMethods: existing.staticMethods + [parsed],
2353+
getters: existing.getters,
2354+
setters: existing.setters,
2355+
documentation: existing.documentation
2356+
)
2357+
} else {
2358+
staticMethodsByType[typeName, default: []].append(parsed)
2359+
}
2360+
} else {
2361+
importedFunctions.append(parsed)
2362+
}
23252363
} else if AttributeChecker.hasJSSetterAttribute(function.attributes) {
23262364
errors.append(
23272365
DiagnosticError(
@@ -2366,7 +2404,8 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
23662404
_ jsFunction: AttributeSyntax,
23672405
_ node: FunctionDeclSyntax,
23682406
enclosingTypeName: String?,
2369-
isStaticMember: Bool
2407+
isStaticMember: Bool,
2408+
includeTypeNameForStatic: Bool = true
23702409
) -> ImportedFunctionSkeleton? {
23712410
guard validateEffects(node.signature.effectSpecifiers, node: node, attributeName: "JSFunction") != nil
23722411
else {
@@ -2377,7 +2416,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
23772416
let jsName = AttributeChecker.extractJSName(from: jsFunction)
23782417
let from = AttributeChecker.extractJSImportFrom(from: jsFunction)
23792418
let name: String
2380-
if isStaticMember, let enclosingTypeName {
2419+
if isStaticMember, includeTypeNameForStatic, let enclosingTypeName {
23812420
name = "\(enclosingTypeName)_\(baseName)"
23822421
} else {
23832422
name = baseName

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2246,6 +2246,14 @@ extension BridgeJSLink {
22462246
)
22472247
}
22482248

2249+
func callStaticMethod(on objectExpr: String, name: String, returnType: BridgeType) throws -> String? {
2250+
let calleeExpr = Self.propertyAccessExpr(objectExpr: objectExpr, propertyName: name)
2251+
return try call(
2252+
calleeExpr: calleeExpr,
2253+
returnType: returnType
2254+
)
2255+
}
2256+
22492257
func callPropertyGetter(name: String, returnType: BridgeType) throws -> String? {
22502258
let objectExpr = "\(JSGlueVariableScope.reservedSwift).memory.getObject(self)"
22512259
let accessExpr = Self.propertyAccessExpr(objectExpr: objectExpr, propertyName: name)
@@ -2318,7 +2326,7 @@ extension BridgeJSLink {
23182326
return loweredValues.first
23192327
}
23202328

2321-
private static func propertyAccessExpr(objectExpr: String, propertyName: String) -> String {
2329+
static func propertyAccessExpr(objectExpr: String, propertyName: String) -> String {
23222330
if propertyName.range(of: #"^[$A-Z_][0-9A-Z_$]*$"#, options: [.regularExpression, .caseInsensitive]) != nil
23232331
{
23242332
return "\(objectExpr).\(propertyName)"
@@ -3130,6 +3138,12 @@ extension BridgeJSLink {
31303138
importObjectBuilder.assignToImportObject(name: setterAbiName, function: js)
31313139
importObjectBuilder.appendDts(dts)
31323140
}
3141+
for method in type.staticMethods {
3142+
let abiName = method.abiName(context: type, operation: "static")
3143+
let (js, dts) = try renderImportedStaticMethod(context: type, method: method)
3144+
importObjectBuilder.assignToImportObject(name: abiName, function: js)
3145+
importObjectBuilder.appendDts(dts)
3146+
}
31333147
for method in type.methods {
31343148
let (js, dts) = try renderImportedMethod(context: type, method: method)
31353149
importObjectBuilder.assignToImportObject(name: method.abiName(context: type), function: js)
@@ -3207,6 +3221,32 @@ extension BridgeJSLink {
32073221
return (funcLines, [])
32083222
}
32093223

3224+
func renderImportedStaticMethod(
3225+
context: ImportedTypeSkeleton,
3226+
method: ImportedFunctionSkeleton
3227+
) throws -> (js: [String], dts: [String]) {
3228+
let thunkBuilder = ImportedThunkBuilder()
3229+
for param in method.parameters {
3230+
try thunkBuilder.liftParameter(param: param)
3231+
}
3232+
let importRootExpr = context.from == .global ? "globalThis" : "imports"
3233+
let constructorExpr = ImportedThunkBuilder.propertyAccessExpr(
3234+
objectExpr: importRootExpr,
3235+
propertyName: context.jsName ?? context.name
3236+
)
3237+
let returnExpr = try thunkBuilder.callStaticMethod(
3238+
on: constructorExpr,
3239+
name: method.jsName ?? method.name,
3240+
returnType: method.returnType
3241+
)
3242+
let funcLines = thunkBuilder.renderFunction(
3243+
name: method.abiName(context: context, operation: "static"),
3244+
returnExpr: returnExpr,
3245+
returnType: method.returnType
3246+
)
3247+
return (funcLines, [])
3248+
}
3249+
32103250
func renderImportedMethod(
32113251
context: ImportedTypeSkeleton,
32123252
method: ImportedFunctionSkeleton

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,9 +640,14 @@ public struct ImportedFunctionSkeleton: Codable {
640640
}
641641

642642
public func abiName(context: ImportedTypeSkeleton?) -> String {
643+
return abiName(context: context, operation: nil)
644+
}
645+
646+
public func abiName(context: ImportedTypeSkeleton?, operation: String?) -> String {
643647
return ABINameGenerator.generateImportedABIName(
644648
baseName: name,
645-
context: context
649+
context: context,
650+
operation: operation
646651
)
647652
}
648653
}
@@ -752,6 +757,8 @@ public struct ImportedTypeSkeleton: Codable {
752757
public let from: JSImportFrom?
753758
public let constructor: ImportedConstructorSkeleton?
754759
public let methods: [ImportedFunctionSkeleton]
760+
/// Static methods available on the JavaScript constructor.
761+
public let staticMethods: [ImportedFunctionSkeleton]
755762
public let getters: [ImportedGetterSkeleton]
756763
public let setters: [ImportedSetterSkeleton]
757764
public let documentation: String?
@@ -762,6 +769,7 @@ public struct ImportedTypeSkeleton: Codable {
762769
from: JSImportFrom? = nil,
763770
constructor: ImportedConstructorSkeleton? = nil,
764771
methods: [ImportedFunctionSkeleton],
772+
staticMethods: [ImportedFunctionSkeleton] = [],
765773
getters: [ImportedGetterSkeleton] = [],
766774
setters: [ImportedSetterSkeleton] = [],
767775
documentation: String? = nil
@@ -771,10 +779,51 @@ public struct ImportedTypeSkeleton: Codable {
771779
self.from = from
772780
self.constructor = constructor
773781
self.methods = methods
782+
self.staticMethods = staticMethods
774783
self.getters = getters
775784
self.setters = setters
776785
self.documentation = documentation
777786
}
787+
788+
private enum CodingKeys: String, CodingKey {
789+
case name
790+
case jsName
791+
case from
792+
case constructor
793+
case methods
794+
case staticMethods
795+
case getters
796+
case setters
797+
case documentation
798+
}
799+
800+
public init(from decoder: any Decoder) throws {
801+
let container = try decoder.container(keyedBy: CodingKeys.self)
802+
self.name = try container.decode(String.self, forKey: .name)
803+
self.jsName = try container.decodeIfPresent(String.self, forKey: .jsName)
804+
self.from = try container.decodeIfPresent(JSImportFrom.self, forKey: .from)
805+
self.constructor = try container.decodeIfPresent(ImportedConstructorSkeleton.self, forKey: .constructor)
806+
self.methods = try container.decode([ImportedFunctionSkeleton].self, forKey: .methods)
807+
self.staticMethods = try container.decodeIfPresent([ImportedFunctionSkeleton].self, forKey: .staticMethods) ?? []
808+
self.getters = try container.decode([ImportedGetterSkeleton].self, forKey: .getters)
809+
self.setters = try container.decode([ImportedSetterSkeleton].self, forKey: .setters)
810+
self.documentation = try container.decodeIfPresent(String.self, forKey: .documentation)
811+
}
812+
813+
public func encode(to encoder: any Encoder) throws {
814+
var container = encoder.container(keyedBy: CodingKeys.self)
815+
try container.encode(name, forKey: .name)
816+
try container.encodeIfPresent(jsName, forKey: .jsName)
817+
try container.encodeIfPresent(from, forKey: .from)
818+
try container.encodeIfPresent(constructor, forKey: .constructor)
819+
try container.encode(methods, forKey: .methods)
820+
if !staticMethods.isEmpty {
821+
try container.encode(staticMethods, forKey: .staticMethods)
822+
}
823+
try container.encode(getters, forKey: .getters)
824+
try container.encode(setters, forKey: .setters)
825+
try container.encodeIfPresent(documentation, forKey: .documentation)
826+
}
778827
}
779828

780829
public struct ImportedFileSkeleton: Codable {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
extension StaticBox {
2+
@JSFunction static func makeDefault() throws(JSException) -> StaticBox
3+
}
4+
5+
@JSClass struct StaticBox {
6+
@JSFunction static func create(_ value: Double) throws(JSException) -> StaticBox
7+
@JSFunction func value() throws(JSException) -> Double
8+
@JSFunction static func value() throws(JSException) -> Double
9+
}
10+
11+
extension StaticBox {
12+
@JSFunction(jsName: "with-dashes") static func dashed() throws(JSException) -> StaticBox
13+
}

0 commit comments

Comments
 (0)