Skip to content

Commit e10836b

Browse files
authored
BridgeJS: Export with a different JS name (#801)
1 parent 30d994d commit e10836b

17 files changed

Lines changed: 2049 additions & 60 deletions

File tree

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,23 @@ public final class SwiftToSkeleton {
731731
return String(name.dropFirst().dropLast())
732732
}
733733

734+
fileprivate static func isValidJSIdentifier(_ name: String) -> Bool {
735+
func isIdentifierPart(_ scalar: Unicode.Scalar, isStart: Bool) -> Bool {
736+
switch scalar {
737+
case "a"..."z", "A"..."Z", "_", "$":
738+
return true
739+
case "0"..."9":
740+
return !isStart
741+
default:
742+
return false
743+
}
744+
}
745+
guard let first = name.unicodeScalars.first, isIdentifierPart(first, isStart: true) else {
746+
return false
747+
}
748+
return name.unicodeScalars.dropFirst().allSatisfy { isIdentifierPart($0, isStart: false) }
749+
}
750+
734751
}
735752

736753
private enum ExportSwiftConstants {
@@ -1291,6 +1308,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
12911308
}
12921309

12931310
let name = node.name.text
1311+
let jsName = extractValidatedJSName(from: jsAttribute)
12941312

12951313
let attributeNamespace = extractNamespace(from: jsAttribute)
12961314
let computedNamespace = computeNamespace(for: node)
@@ -1378,7 +1396,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
13781396
classNameForABI = nil
13791397
}
13801398
abiName = ABINameGenerator.generateABIName(
1381-
baseName: name,
1399+
baseName: jsName ?? name,
13821400
namespace: finalNamespace,
13831401
staticContext: isStatic ? staticContext : nil,
13841402
className: classNameForABI
@@ -1390,6 +1408,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
13901408

13911409
return ExportedFunction(
13921410
name: name,
1411+
jsName: jsName,
13931412
abiName: abiName,
13941413
parameters: parameters,
13951414
returnType: returnType,
@@ -1469,6 +1488,45 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
14691488
return Effects(isAsync: isAsync, isThrows: isThrows, isStatic: isStatic)
14701489
}
14711490

1491+
private func extractJSName(
1492+
from jsAttribute: AttributeSyntax
1493+
) -> String? {
1494+
guard let arguments = jsAttribute.arguments?.as(LabeledExprListSyntax.self),
1495+
let nameArg = arguments.first,
1496+
nameArg.label == nil,
1497+
let stringLiteral = nameArg.expression.as(StringLiteralExprSyntax.self),
1498+
stringLiteral.segments.count == 1,
1499+
let name = stringLiteral.segments.first?.as(StringSegmentSyntax.self)?.content.text
1500+
else {
1501+
return nil
1502+
}
1503+
return name
1504+
}
1505+
1506+
private func extractValidatedJSName(
1507+
from jsAttribute: AttributeSyntax
1508+
) -> String? {
1509+
guard let jsName = extractJSName(from: jsAttribute) else { return nil }
1510+
guard SwiftToSkeleton.isValidJSIdentifier(jsName) else {
1511+
diagnose(
1512+
node: jsAttribute,
1513+
message: "`\(jsName)` is not a valid JavaScript identifier"
1514+
)
1515+
return nil
1516+
}
1517+
return jsName
1518+
}
1519+
1520+
private func diagnoseUnsupportedJSName(
1521+
from jsAttribute: AttributeSyntax
1522+
) {
1523+
guard extractJSName(from: jsAttribute) != nil else { return }
1524+
diagnose(
1525+
node: jsAttribute,
1526+
message: "A separate name for JavaScript is not supported here"
1527+
)
1528+
}
1529+
14721530
private func extractNamespace(
14731531
from jsAttribute: AttributeSyntax
14741532
) -> [String]? {
@@ -1515,6 +1573,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
15151573
override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
15161574
guard let jsAttribute = node.attributes.firstJSAttribute else { return .skipChildren }
15171575

1576+
diagnoseUnsupportedJSName(from: jsAttribute)
1577+
15181578
switch state {
15191579
case .classBody(_, let classKey):
15201580
if extractNamespace(from: jsAttribute) != nil {
@@ -1636,6 +1696,15 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
16361696
}
16371697
}
16381698

1699+
let jsName = extractValidatedJSName(from: jsAttribute)
1700+
if jsName != nil, node.bindings.count > 1 {
1701+
diagnose(
1702+
node: jsAttribute,
1703+
message: "Name targets declaration with multiple bindings",
1704+
hint: "Declare each property with a different JS name separately"
1705+
)
1706+
}
1707+
16391708
// Process each binding (variable declaration)
16401709
for binding in node.bindings {
16411710
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else {
@@ -1663,6 +1732,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
16631732

16641733
let exportedProperty = ExportedProperty(
16651734
name: propertyName,
1735+
jsName: jsName,
16661736
type: propertyType,
16671737
isReadonly: isReadonly,
16681738
isStatic: isStatic,
@@ -1693,6 +1763,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
16931763
return .skipChildren
16941764
}
16951765

1766+
diagnoseUnsupportedJSName(from: jsAttribute)
1767+
16961768
if let aliasTarget = parent.extractAliasTarget(from: jsAttribute) {
16971769
recordAlias(node: node, jsAttribute: jsAttribute, aliasTarget: aliasTarget)
16981770
return .skipChildren
@@ -1843,6 +1915,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
18431915
return .skipChildren
18441916
}
18451917

1918+
diagnoseUnsupportedJSName(from: jsAttribute)
1919+
18461920
if let aliasTarget = parent.extractAliasTarget(from: jsAttribute) {
18471921
recordAlias(node: node, jsAttribute: jsAttribute, aliasTarget: aliasTarget)
18481922
return .skipChildren
@@ -1968,6 +2042,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
19682042
return .skipChildren
19692043
}
19702044

2045+
diagnoseUnsupportedJSName(from: jsAttribute)
2046+
19712047
let name = node.name.text
19722048

19732049
let namespaceResult = resolveNamespace(from: jsAttribute, for: node, declarationType: "protocol")
@@ -2031,6 +2107,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
20312107
return .skipChildren
20322108
}
20332109

2110+
diagnoseUnsupportedJSName(from: jsAttribute)
2111+
20342112
if let aliasTarget = parent.extractAliasTarget(from: jsAttribute) {
20352113
recordAlias(node: node, jsAttribute: jsAttribute, aliasTarget: aliasTarget)
20362114
return .skipChildren
@@ -2169,6 +2247,10 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
21692247
protocolName: String,
21702248
namespace: [String]?
21712249
) -> ExportedFunction? {
2250+
if let jsAttribute = node.attributes.firstJSAttribute {
2251+
diagnoseUnsupportedJSName(from: jsAttribute)
2252+
}
2253+
21722254
let name = node.name.text
21732255

21742256
let parameters = parseParameters(from: node.signature.parameterClause, allowDefaults: false)
@@ -2215,6 +2297,10 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
22152297
protocolName: String,
22162298
protocolKey: String
22172299
) -> SyntaxVisitorContinueKind {
2300+
if let jsAttribute = node.attributes.firstJSAttribute {
2301+
diagnoseUnsupportedJSName(from: jsAttribute)
2302+
}
2303+
22182304
for binding in node.bindings {
22192305
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else {
22202306
diagnose(node: binding.pattern, message: "Complex patterns not supported for protocol properties")

0 commit comments

Comments
 (0)