Skip to content

Commit 8cad280

Browse files
committed
BridgeJS: Add support for static / class functions
1 parent 68466e1 commit 8cad280

30 files changed

+2166
-69
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 148 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public class ExportSwift {
9393
var name: String?
9494
var cases: [EnumCase] = []
9595
var rawType: String?
96+
var staticMethods: [ExportedFunction] = []
9697
}
9798
var currentEnum = CurrentEnum()
9899

@@ -152,28 +153,54 @@ public class ExportSwift {
152153
}
153154

154155
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
156+
guard node.attributes.hasJSAttribute() else {
157+
return .skipChildren
158+
}
159+
160+
let isStatic = node.modifiers.contains { modifier in
161+
modifier.name.tokenKind == .keyword(.static) ||
162+
modifier.name.tokenKind == .keyword(.class)
163+
}
164+
155165
switch state {
156166
case .topLevel:
157-
if let exportedFunction = visitFunction(
158-
node: node
159-
) {
167+
if isStatic {
168+
diagnose(node: node, message: "Top-level functions cannot be static")
169+
return .skipChildren
170+
}
171+
if let exportedFunction = visitFunction(node: node, isStatic: false) {
160172
exportedFunctions.append(exportedFunction)
161173
}
162174
return .skipChildren
163-
case .classBody(_, let classKey):
175+
case .classBody(let className, let classKey):
164176
if let exportedFunction = visitFunction(
165-
node: node
177+
node: node,
178+
isStatic: isStatic,
179+
className: className,
180+
classKey: classKey
166181
) {
167182
exportedClassByName[classKey]?.methods.append(exportedFunction)
168183
}
169184
return .skipChildren
170-
case .enumBody:
171-
diagnose(node: node, message: "Functions are not supported inside enums")
185+
case .enumBody(let enumName):
186+
if !isStatic {
187+
diagnose(node: node, message: "Only static functions are supported in enums")
188+
return .skipChildren
189+
}
190+
if let exportedFunction = visitFunction(node: node, isStatic: isStatic, enumName: enumName) {
191+
currentEnum.staticMethods.append(exportedFunction)
192+
}
172193
return .skipChildren
173194
}
174195
}
175196

176-
private func visitFunction(node: FunctionDeclSyntax) -> ExportedFunction? {
197+
private func visitFunction(
198+
node: FunctionDeclSyntax,
199+
isStatic: Bool,
200+
className: String? = nil,
201+
classKey: String? = nil,
202+
enumName: String? = nil
203+
) -> ExportedFunction? {
177204
guard let jsAttribute = node.attributes.firstJSAttribute else {
178205
return nil
179206
}
@@ -189,6 +216,14 @@ public class ExportSwift {
189216
)
190217
}
191218

219+
if namespace != nil, case .enumBody = state {
220+
diagnose(
221+
node: jsAttribute,
222+
message: "Namespace is not supported for enum static functions",
223+
hint: "Remove the namespace from @JS attribute - enum functions inherit namespace from enum"
224+
)
225+
}
226+
192227
var parameters: [Parameter] = []
193228
for param in node.signature.parameterClause.parameters {
194229
let resolvedType = self.parent.lookupType(for: param.type)
@@ -226,20 +261,52 @@ public class ExportSwift {
226261
}
227262

228263
let abiName: String
264+
let staticContext: StaticContext?
265+
229266
switch state {
230267
case .topLevel:
231268
abiName = "bjs_\(name)"
269+
staticContext = nil
232270
case .classBody(let className, _):
233-
abiName = "bjs_\(className)_\(name)"
234-
case .enumBody:
235-
abiName = ""
236-
diagnose(
237-
node: node,
238-
message: "Functions are not supported inside enums"
239-
)
271+
if isStatic {
272+
abiName = "bjs_\(className)_static_\(name)"
273+
staticContext = .className(className)
274+
} else {
275+
abiName = "bjs_\(className)_\(name)"
276+
staticContext = nil
277+
}
278+
case .enumBody(let enumName):
279+
if !isStatic {
280+
diagnose(node: node, message: "Only static functions are supported in enums")
281+
return nil
282+
}
283+
284+
let isNamespaceEnum = currentEnum.cases.isEmpty
285+
286+
if isNamespaceEnum {
287+
// For namespace enums, compute the full Swift call path manually
288+
var swiftPath: [String] = []
289+
var currentNode: Syntax? = node.parent
290+
while let parent = currentNode {
291+
if let enumDecl = parent.as(EnumDeclSyntax.self),
292+
enumDecl.attributes.hasJSAttribute()
293+
{
294+
swiftPath.insert(enumDecl.name.text, at: 0)
295+
}
296+
currentNode = parent.parent
297+
}
298+
let fullEnumCallName = swiftPath.joined(separator: ".")
299+
300+
// ABI name should include full namespace path to avoid conflicts
301+
abiName = "bjs_\(swiftPath.joined(separator: "_"))_\(name)"
302+
staticContext = .namespaceEnum(fullEnumCallName)
303+
} else {
304+
abiName = "bjs_\(enumName)_static_\(name)"
305+
staticContext = .enumName(enumName)
306+
}
240307
}
241308

242-
guard let effects = collectEffects(signature: node.signature) else {
309+
guard let effects = collectEffects(signature: node.signature, isStatic: isStatic) else {
243310
return nil
244311
}
245312

@@ -249,11 +316,12 @@ public class ExportSwift {
249316
parameters: parameters,
250317
returnType: returnType,
251318
effects: effects,
252-
namespace: namespace
319+
namespace: namespace,
320+
staticContext: staticContext
253321
)
254322
}
255323

256-
private func collectEffects(signature: FunctionSignatureSyntax) -> Effects? {
324+
private func collectEffects(signature: FunctionSignatureSyntax, isStatic: Bool = false) -> Effects? {
257325
let isAsync = signature.effectSpecifiers?.asyncSpecifier != nil
258326
var isThrows = false
259327
if let throwsClause: ThrowsClauseSyntax = signature.effectSpecifiers?.throwsClause {
@@ -274,7 +342,7 @@ public class ExportSwift {
274342
}
275343
isThrows = true
276344
}
277-
return Effects(isAsync: isAsync, isThrows: isThrows)
345+
return Effects(isAsync: isAsync, isThrows: isThrows, isStatic: isStatic)
278346
}
279347

280348
private func extractNamespace(
@@ -537,15 +605,25 @@ public class ExportSwift {
537605
}
538606

539607
let emitStyle = extractEnumStyle(from: jsAttribute) ?? .const
540-
if case .tsEnum = emitStyle,
541-
let raw = currentEnum.rawType,
542-
let rawEnum = SwiftEnumRawType.from(raw), rawEnum == .bool
543-
{
544-
diagnose(
545-
node: jsAttribute,
546-
message: "TypeScript enum style is not supported for Bool raw-value enums",
547-
hint: "Use enumStyle: .const or change the raw type to String or a numeric type"
548-
)
608+
609+
if case .tsEnum = emitStyle {
610+
if let raw = currentEnum.rawType,
611+
let rawEnum = SwiftEnumRawType.from(raw), rawEnum == .bool
612+
{
613+
diagnose(
614+
node: jsAttribute,
615+
message: "TypeScript enum style is not supported for Bool raw-value enums",
616+
hint: "Use enumStyle: .const or change the raw type to String or a numeric type"
617+
)
618+
}
619+
620+
if !currentEnum.staticMethods.isEmpty {
621+
diagnose(
622+
node: jsAttribute,
623+
message: "TypeScript enum style does not support static functions",
624+
hint: "Use enumStyle: .const to generate a const object that supports static functions"
625+
)
626+
}
549627
}
550628

551629
if currentEnum.cases.contains(where: { !$0.associatedValues.isEmpty }) {
@@ -597,7 +675,8 @@ public class ExportSwift {
597675
cases: currentEnum.cases,
598676
rawType: currentEnum.rawType,
599677
namespace: effectiveNamespace,
600-
emitStyle: emitStyle
678+
emitStyle: emitStyle,
679+
staticMethods: currentEnum.staticMethods
601680
)
602681
exportedEnumByName[enumName] = exportedEnum
603682
exportedEnumNames.append(enumName)
@@ -862,6 +941,10 @@ public class ExportSwift {
862941
case .namespace:
863942
()
864943
}
944+
945+
for staticMethod in enumDef.staticMethods {
946+
decls.append(try renderSingleExportedFunction(function: staticMethod))
947+
}
865948
}
866949

867950
for function in exportedFunctions {
@@ -1269,7 +1352,24 @@ public class ExportSwift {
12691352
for param in function.parameters {
12701353
try builder.liftParameter(param: param)
12711354
}
1272-
builder.call(name: function.name, returnType: function.returnType)
1355+
1356+
if function.effects.isStatic, let staticContext = function.staticContext {
1357+
let callName: String
1358+
switch staticContext {
1359+
case .className(let className):
1360+
callName = "\(className).\(function.name)"
1361+
case .enumName(let enumName):
1362+
callName = "\(enumName).\(function.name)"
1363+
case .namespaceEnum(let enumName):
1364+
callName = "\(enumName).\(function.name)"
1365+
case .explicitNamespace(let namespace):
1366+
callName = "\(namespace.joined(separator: ".")).\(function.name)"
1367+
}
1368+
builder.call(name: callName, returnType: function.returnType)
1369+
} else {
1370+
builder.call(name: function.name, returnType: function.returnType)
1371+
}
1372+
12731373
try builder.lowerReturnValue(returnType: function.returnType)
12741374
return builder.render(abiName: function.abiName)
12751375
}
@@ -1335,17 +1435,25 @@ public class ExportSwift {
13351435
}
13361436
for method in klass.methods {
13371437
let builder = ExportedThunkBuilder(effects: method.effects)
1338-
try builder.liftParameter(
1339-
param: Parameter(label: nil, name: "_self", type: BridgeType.swiftHeapObject(klass.swiftCallName))
1340-
)
1341-
for param in method.parameters {
1342-
try builder.liftParameter(param: param)
1438+
1439+
if method.effects.isStatic {
1440+
for param in method.parameters {
1441+
try builder.liftParameter(param: param)
1442+
}
1443+
builder.call(name: "\(klass.swiftCallName).\(method.name)", returnType: method.returnType)
1444+
} else {
1445+
try builder.liftParameter(
1446+
param: Parameter(label: nil, name: "_self", type: BridgeType.swiftHeapObject(klass.swiftCallName))
1447+
)
1448+
for param in method.parameters {
1449+
try builder.liftParameter(param: param)
1450+
}
1451+
builder.callMethod(
1452+
klassName: klass.swiftCallName,
1453+
methodName: method.name,
1454+
returnType: method.returnType
1455+
)
13431456
}
1344-
builder.callMethod(
1345-
klassName: klass.swiftCallName,
1346-
methodName: method.name,
1347-
returnType: method.returnType
1348-
)
13491457
try builder.lowerReturnValue(returnType: method.returnType)
13501458
decls.append(builder.render(abiName: method.abiName))
13511459
}

0 commit comments

Comments
 (0)