@@ -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
736753private 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