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