Skip to content

Commit c446901

Browse files
BridgeJS: Support @JSFunction static func
1 parent edabb97 commit c446901

File tree

20 files changed

+615
-9
lines changed

20 files changed

+615
-9
lines changed

Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@
191191
"name" : "TS2Swift",
192192
"setters" : [
193193

194+
],
195+
"staticMethods" : [
196+
194197
]
195198
}
196199
]

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: 10 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 var 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,6 +779,7 @@ 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

Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -821,8 +821,12 @@ export class TypeProcessor {
821821
const returnType = this.visitType(signature.getReturnType(), node);
822822
const effects = this.renderEffects({ isAsync: false });
823823
const swiftMethodName = this.renderIdentifier(swiftName);
824+
const isStatic = node.modifiers?.some(
825+
(modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword
826+
) ?? false;
827+
const staticKeyword = isStatic ? "static " : "";
824828

825-
this.swiftLines.push(` ${annotation} func ${swiftMethodName}(${params}) ${effects} -> ${returnType}`);
829+
this.swiftLines.push(` ${annotation} ${staticKeyword}func ${swiftMethodName}(${params}) ${effects} -> ${returnType}`);
826830
}
827831

828832
/**
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+
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/GlobalGetter.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@
6161
"name" : "JSConsole",
6262
"setters" : [
6363

64+
],
65+
"staticMethods" : [
66+
6467
]
6568
}
6669
]

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/GlobalThisImports.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@
8181
"name" : "JSConsole",
8282
"setters" : [
8383

84+
],
85+
"staticMethods" : [
86+
8487
]
8588
},
8689
{
@@ -116,6 +119,9 @@
116119
"name" : "WebSocket",
117120
"setters" : [
118121

122+
],
123+
"staticMethods" : [
124+
119125
]
120126
}
121127
]

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/ImportedTypeInExportedInterface.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@
186186
"name" : "Foo",
187187
"setters" : [
188188

189+
],
190+
"staticMethods" : [
191+
189192
]
190193
}
191194
]

0 commit comments

Comments
 (0)