Skip to content

Commit e4b688f

Browse files
authored
Merge pull request #568 from swiftwasm/katei/8891-bridgejs-support
BridgeJS: Support static @JSFunction imports
2 parents 89ed56e + 5161a41 commit e4b688f

File tree

24 files changed

+1002
-72
lines changed

24 files changed

+1002
-72
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: 11 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,7 +1856,6 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
18561856
private let inputFilePath: String
18571857
private var jsClassNames: Set<String>
18581858
private let parent: SwiftToSkeleton
1859-
18601859
// MARK: - State Management
18611860

18621861
enum State {
@@ -1876,6 +1875,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
18761875
let from: JSImportFrom?
18771876
var constructor: ImportedConstructorSkeleton?
18781877
var methods: [ImportedFunctionSkeleton]
1878+
var staticMethods: [ImportedFunctionSkeleton]
18791879
var getters: [ImportedGetterSkeleton]
18801880
var setters: [ImportedSetterSkeleton]
18811881
}
@@ -2094,6 +2094,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
20942094
from: nil,
20952095
constructor: nil,
20962096
methods: [],
2097+
staticMethods: [],
20972098
getters: [],
20982099
setters: []
20992100
)
@@ -2107,6 +2108,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
21072108
from: from,
21082109
constructor: nil,
21092110
methods: [],
2111+
staticMethods: [],
21102112
getters: [],
21112113
setters: []
21122114
)
@@ -2121,6 +2123,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
21212123
from: type.from,
21222124
constructor: type.constructor,
21232125
methods: type.methods,
2126+
staticMethods: type.staticMethods,
21242127
getters: type.getters,
21252128
setters: type.setters,
21262129
documentation: nil
@@ -2165,12 +2168,6 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
21652168

21662169
// MARK: - Visitor Methods
21672170

2168-
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
2169-
let typeName = node.extendedType.trimmedDescription
2170-
collectStaticMembers(in: node.memberBlock.members, typeName: typeName)
2171-
return .skipChildren
2172-
}
2173-
21742171
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
21752172
switch state {
21762173
case .topLevel:
@@ -2191,7 +2188,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
21912188

21922189
private func handleTopLevelFunction(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
21932190
if let jsFunction = AttributeChecker.firstJSFunctionAttribute(node.attributes),
2194-
let function = parseFunction(jsFunction, node, enclosingTypeName: nil, isStaticMember: true)
2191+
let function = parseFunction(jsFunction, node)
21952192
{
21962193
importedFunctions.append(function)
21972194
return .skipChildren
@@ -2216,13 +2213,11 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
22162213
type: inout CurrentType
22172214
) -> Bool {
22182215
if let jsFunction = AttributeChecker.firstJSFunctionAttribute(node.attributes) {
2219-
if isStaticMember {
2220-
parseFunction(jsFunction, node, enclosingTypeName: typeName, isStaticMember: true).map {
2221-
importedFunctions.append($0)
2222-
}
2223-
} else {
2224-
parseFunction(jsFunction, node, enclosingTypeName: typeName, isStaticMember: false).map {
2225-
type.methods.append($0)
2216+
if let method = parseFunction(jsFunction, node) {
2217+
if isStaticMember {
2218+
type.staticMethods.append(method)
2219+
} else {
2220+
type.methods.append(method)
22262221
}
22272222
}
22282223
return true
@@ -2313,38 +2308,6 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
23132308
}
23142309
}
23152310

2316-
// MARK: - Member Collection
2317-
2318-
private func collectStaticMembers(in members: MemberBlockItemListSyntax, typeName: String) {
2319-
for member in members {
2320-
if let function = member.decl.as(FunctionDeclSyntax.self) {
2321-
if let jsFunction = AttributeChecker.firstJSFunctionAttribute(function.attributes),
2322-
let parsed = parseFunction(jsFunction, function, enclosingTypeName: typeName, isStaticMember: true)
2323-
{
2324-
importedFunctions.append(parsed)
2325-
} else if AttributeChecker.hasJSSetterAttribute(function.attributes) {
2326-
errors.append(
2327-
DiagnosticError(
2328-
node: function,
2329-
message:
2330-
"@JSSetter is not supported for static members. Use it only for instance members in @JSClass types."
2331-
)
2332-
)
2333-
}
2334-
} else if let variable = member.decl.as(VariableDeclSyntax.self),
2335-
AttributeChecker.hasJSGetterAttribute(variable.attributes)
2336-
{
2337-
errors.append(
2338-
DiagnosticError(
2339-
node: variable,
2340-
message:
2341-
"@JSGetter is not supported for static members. Use it only for instance members in @JSClass types."
2342-
)
2343-
)
2344-
}
2345-
}
2346-
}
2347-
23482311
// MARK: - Parsing Methods
23492312

23502313
private func parseConstructor(
@@ -2365,8 +2328,6 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
23652328
private func parseFunction(
23662329
_ jsFunction: AttributeSyntax,
23672330
_ node: FunctionDeclSyntax,
2368-
enclosingTypeName: String?,
2369-
isStaticMember: Bool
23702331
) -> ImportedFunctionSkeleton? {
23712332
guard validateEffects(node.signature.effectSpecifiers, node: node, attributeName: "JSFunction") != nil
23722333
else {
@@ -2376,12 +2337,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
23762337
let baseName = SwiftToSkeleton.normalizeIdentifier(node.name.text)
23772338
let jsName = AttributeChecker.extractJSName(from: jsFunction)
23782339
let from = AttributeChecker.extractJSImportFrom(from: jsFunction)
2379-
let name: String
2380-
if isStaticMember, let enclosingTypeName {
2381-
name = "\(enclosingTypeName)_\(baseName)"
2382-
} else {
2383-
name = baseName
2384-
}
2340+
let name = baseName
23852341

23862342
let parameters = parseParameters(from: node.signature.parameterClause)
23872343
let returnType: BridgeType

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 61 additions & 14 deletions
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,32 @@ 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+
}
3147+
if type.from == nil, type.constructor != nil || !type.staticMethods.isEmpty {
3148+
let dtsPrinter = CodeFragmentPrinter()
3149+
dtsPrinter.write("\(type.name): {")
3150+
dtsPrinter.indent {
3151+
if let constructor = type.constructor {
3152+
let returnType = BridgeType.jsObject(type.name)
3153+
dtsPrinter.write(
3154+
"new\(renderTSSignature(parameters: constructor.parameters, returnType: returnType, effects: Effects(isAsync: false, isThrows: false)));"
3155+
)
3156+
}
3157+
for method in type.staticMethods {
3158+
let methodName = method.jsName ?? method.name
3159+
let signature =
3160+
"\(renderTSPropertyName(methodName))\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: Effects(isAsync: false, isThrows: false)));"
3161+
dtsPrinter.write(signature)
3162+
}
3163+
}
3164+
dtsPrinter.write("}")
3165+
importObjectBuilder.appendDts(dtsPrinter.lines)
3166+
}
31333167
for method in type.methods {
31343168
let (js, dts) = try renderImportedMethod(context: type, method: method)
31353169
importObjectBuilder.assignToImportObject(name: method.abiName(context: type), function: js)
@@ -3160,19 +3194,6 @@ extension BridgeJSLink {
31603194
returnType: returnType
31613195
)
31623196
importObjectBuilder.assignToImportObject(name: abiName, function: funcLines)
3163-
3164-
if type.from == nil {
3165-
let dtsPrinter = CodeFragmentPrinter()
3166-
dtsPrinter.write("\(type.name): {")
3167-
dtsPrinter.indent {
3168-
dtsPrinter.write(
3169-
"new\(renderTSSignature(parameters: constructor.parameters, returnType: returnType, effects: Effects(isAsync: false, isThrows: false)));"
3170-
)
3171-
}
3172-
dtsPrinter.write("}")
3173-
3174-
importObjectBuilder.appendDts(dtsPrinter.lines)
3175-
}
31763197
}
31773198

31783199
func renderImportedGetter(
@@ -3207,6 +3228,32 @@ extension BridgeJSLink {
32073228
return (funcLines, [])
32083229
}
32093230

3231+
func renderImportedStaticMethod(
3232+
context: ImportedTypeSkeleton,
3233+
method: ImportedFunctionSkeleton
3234+
) throws -> (js: [String], dts: [String]) {
3235+
let thunkBuilder = ImportedThunkBuilder()
3236+
for param in method.parameters {
3237+
try thunkBuilder.liftParameter(param: param)
3238+
}
3239+
let importRootExpr = context.from == .global ? "globalThis" : "imports"
3240+
let constructorExpr = ImportedThunkBuilder.propertyAccessExpr(
3241+
objectExpr: importRootExpr,
3242+
propertyName: context.jsName ?? context.name
3243+
)
3244+
let returnExpr = try thunkBuilder.callStaticMethod(
3245+
on: constructorExpr,
3246+
name: method.jsName ?? method.name,
3247+
returnType: method.returnType
3248+
)
3249+
let funcLines = thunkBuilder.renderFunction(
3250+
name: method.abiName(context: context, operation: "static"),
3251+
returnExpr: returnExpr,
3252+
returnType: method.returnType
3253+
)
3254+
return (funcLines, [])
3255+
}
3256+
32103257
func renderImportedMethod(
32113258
context: ImportedTypeSkeleton,
32123259
method: ImportedFunctionSkeleton

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 11 additions & 2 deletions
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?
@@ -761,7 +768,8 @@ public struct ImportedTypeSkeleton: Codable {
761768
jsName: String? = nil,
762769
from: JSImportFrom? = nil,
763770
constructor: ImportedConstructorSkeleton? = nil,
764-
methods: [ImportedFunctionSkeleton],
771+
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
/**

Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ exports[`ts2swift > snapshots Swift output for TypeScriptClass.d.ts > TypeScript
295295
@JSFunction init(_ name: String) throws(JSException)
296296
@JSFunction func greet() throws(JSException) -> String
297297
@JSFunction func changeName(_ name: String) throws(JSException) -> Void
298+
@JSFunction static func staticMethod(_ p1: Double, _ p2: String) throws(JSException) -> String
298299
}
299300
"
300301
`;

Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/fixtures/TypeScriptClass.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ export class Greeter {
44
constructor(name: string);
55
greet(): string;
66
changeName(name: string): void;
7+
8+
static staticMethod(p1: number, p2: string): string;
79
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@JSClass struct StaticBox {
2+
@JSFunction static func create(_ value: Double) throws(JSException) -> StaticBox
3+
@JSFunction func value() throws(JSException) -> Double
4+
@JSFunction static func value() throws(JSException) -> Double
5+
@JSFunction static func makeDefault() throws(JSException) -> StaticBox
6+
@JSFunction(jsName: "with-dashes") static func dashed() throws(JSException) -> StaticBox
7+
}
8+
9+
@JSClass struct WithCtor {
10+
@JSFunction init(_ value: Double) throws(JSException)
11+
@JSFunction static func create(_ value: Double) throws(JSException) -> WithCtor
12+
}

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
]

0 commit comments

Comments
 (0)