Skip to content

Commit 2dd3e86

Browse files
committed
BridgeJS: Support custom exported type names
1 parent ebc501f commit 2dd3e86

28 files changed

Lines changed: 6791 additions & 216 deletions

File tree

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 91 additions & 73 deletions
Large diffs are not rendered by default.

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 319 additions & 66 deletions
Large diffs are not rendered by default.

Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ final class JSGlueVariableScope {
4343

4444
private let intrinsicRegistry: JSIntrinsicRegistry
4545

46-
private var variables: Set<String> = [
46+
static let reservedVariables: Set<String> = [
4747
reservedSwift,
4848
reservedInstance,
4949
reservedMemory,
@@ -77,6 +77,8 @@ final class JSGlueVariableScope {
7777
reservedRegisterTypeHandles,
7878
]
7979

80+
private var variables = JSGlueVariableScope.reservedVariables
81+
8082
init(intrinsicRegistry: JSIntrinsicRegistry) {
8183
self.intrinsicRegistry = intrinsicRegistry
8284
}
@@ -110,6 +112,14 @@ final class JSGlueVariableScope {
110112
intrinsicRegistry.typeOwnerModules[typeName]
111113
}
112114

115+
func exportedClassPath(forSwiftName name: String) -> [String]? {
116+
intrinsicRegistry.classPaths[name]
117+
}
118+
119+
func renamedEnumNames(forSwiftName name: String) -> (value: String, type: String)? {
120+
intrinsicRegistry.renamedEnumNames[name]
121+
}
122+
113123
func makeChildScope() -> JSGlueVariableScope {
114124
JSGlueVariableScope(intrinsicRegistry: intrinsicRegistry)
115125
}
@@ -554,12 +564,31 @@ struct IntrinsicJSFragment: Sendable {
554564
}
555565

556566
func classReference(forQualifiedName qualifiedName: String) -> String {
567+
if let path = scope.exportedClassPath(forSwiftName: qualifiedName), let name = path.last {
568+
if hasDirectAccessToSwiftClass {
569+
return name
570+
}
571+
return path.count == 1 ? "_exports['\(name)']" : "_exports.\(path.joined(separator: "."))"
572+
}
557573
if hasDirectAccessToSwiftClass {
558574
return unqualifiedClassName(for: qualifiedName)
559575
}
560576
let unqualified = unqualifiedClassName(for: qualifiedName)
561577
return exportsAccess(forClass: unqualified)
562578
}
579+
580+
func defaultValueTypeName(_ type: BridgeType, _ format: DefaultValueUtils.OutputFormat) -> String? {
581+
switch type {
582+
case .swiftHeapObject(let name):
583+
guard let jsName = scope.exportedClassPath(forSwiftName: name)?.last else { return nil }
584+
return format == .javascript ? classReference(forQualifiedName: name) : jsName
585+
case .caseEnum(let name):
586+
guard let renamed = scope.renamedEnumNames(forSwiftName: name) else { return nil }
587+
return format == .javascript ? renamed.value : renamed.type
588+
default:
589+
return nil
590+
}
591+
}
563592
}
564593

565594
/// A function that prints the fragment code.
@@ -2565,7 +2594,10 @@ struct IntrinsicJSFragment: Sendable {
25652594

25662595
// Attach instance methods to the struct instance
25672596
for method in structDef.methods where !method.effects.isStatic {
2568-
let paramList = DefaultValueUtils.formatParameterList(method.parameters)
2597+
let paramList = DefaultValueUtils.formatParameterList(
2598+
method.parameters,
2599+
resolveTypeName: context.defaultValueTypeName
2600+
)
25692601
printer.write(
25702602
"\(instanceVar).\(method.resolvedJSName) = function(\(paramList)) {"
25712603
)

Plugins/BridgeJS/Sources/BridgeJSLink/JSIntrinsicRegistry.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import BridgeJSUtilities
66
final class JSIntrinsicRegistry {
77
private var entries: [String: [String]] = [:]
88
var classNamespaces: [String: [String]] = [:]
9+
var classPaths: [String: [String]] = [:]
10+
var renamedEnumNames: [String: (value: String, type: String)] = [:]
911

1012
var typeOwnerModules: [String: String] = [:]
1113

@@ -43,6 +45,8 @@ final class JSIntrinsicRegistry {
4345
func reset() {
4446
entries.removeAll()
4547
classNamespaces.removeAll()
48+
classPaths.removeAll()
49+
renamedEnumNames.removeAll()
4650
typeOwnerModules.removeAll()
4751
codecNameOrder.removeAll()
4852
codecBodies.removeAll()

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@
44

55
public protocol NamespacedExportedType {
66
var name: String { get }
7+
var jsName: String? { get }
8+
/// The namespace using Swift declaration names, retained for ABI generation.
79
var namespace: [String]? { get }
10+
/// The public namespace when an enclosing declaration has an exported name override.
11+
var jsNamespace: [String]? { get }
812
}
913

1014
extension NamespacedExportedType {
15+
public var resolvedJSName: String { jsName ?? name }
16+
public var resolvedJSNamespace: [String]? { jsNamespace ?? namespace }
17+
1118
public var abiName: String {
1219
if let namespace = namespace, !namespace.isEmpty {
1320
return (namespace + [name]).joined(separator: "_")
@@ -16,7 +23,7 @@ extension NamespacedExportedType {
1623
}
1724

1825
public var tsPathComponents: [String] {
19-
(namespace ?? []) + [name]
26+
(resolvedJSNamespace ?? []) + [resolvedJSName]
2027
}
2128

2229
public var tsFullPath: String {
@@ -778,31 +785,37 @@ public struct StructField: Codable, Equatable, Sendable {
778785

779786
public struct ExportedStruct: Codable, Equatable, Sendable, NamespacedExportedType {
780787
public let name: String
788+
public let jsName: String?
781789
public let swiftCallName: String
782790
public let explicitAccessControl: String?
783791
public var properties: [ExportedProperty]
784792
public var constructor: ExportedConstructor?
785793
public var methods: [ExportedFunction]
786794
public let namespace: [String]?
795+
public let jsNamespace: [String]?
787796
public var documentation: String?
788797

789798
public init(
790799
name: String,
800+
jsName: String? = nil,
791801
swiftCallName: String,
792802
explicitAccessControl: String?,
793803
properties: [ExportedProperty] = [],
794804
constructor: ExportedConstructor? = nil,
795805
methods: [ExportedFunction] = [],
796806
namespace: [String]?,
807+
jsNamespace: [String]? = nil,
797808
documentation: String? = nil
798809
) {
799810
self.name = name
811+
self.jsName = jsName
800812
self.swiftCallName = swiftCallName
801813
self.explicitAccessControl = explicitAccessControl
802814
self.properties = properties
803815
self.constructor = constructor
804816
self.methods = methods
805817
self.namespace = namespace
818+
self.jsNamespace = jsNamespace
806819
self.documentation = documentation
807820
}
808821
}
@@ -864,12 +877,14 @@ public struct ExportedEnum: Codable, Equatable, Sendable, NamespacedExportedType
864877
public static let objectSuffix = "Object"
865878

866879
public let name: String
880+
public let jsName: String?
867881
public let swiftCallName: String
868882
public let tsFullPath: String
869883
public let explicitAccessControl: String?
870884
public var cases: [EnumCase]
871885
public let rawType: SwiftEnumRawType?
872886
public let namespace: [String]?
887+
public let jsNamespace: [String]?
873888
public let emitStyle: EnumEmitStyle
874889
public var staticMethods: [ExportedFunction]
875890
public var staticProperties: [ExportedProperty] = []
@@ -885,33 +900,39 @@ public struct ExportedEnum: Codable, Equatable, Sendable, NamespacedExportedType
885900
}
886901

887902
public var valuesName: String {
888-
emitStyle == .tsEnum ? name : "\(name)\(Self.valuesSuffix)"
903+
emitStyle == .tsEnum ? resolvedJSName : "\(resolvedJSName)\(Self.valuesSuffix)"
889904
}
890905

891906
public var objectTypeName: String {
892-
"\(name)\(Self.objectSuffix)"
907+
"\(resolvedJSName)\(Self.objectSuffix)"
893908
}
894909

895910
public init(
896911
name: String,
912+
jsName: String? = nil,
897913
swiftCallName: String,
898914
tsFullPath: String,
899915
explicitAccessControl: String?,
900916
cases: [EnumCase],
901917
rawType: SwiftEnumRawType?,
902918
namespace: [String]?,
919+
jsNamespace: [String]? = nil,
903920
emitStyle: EnumEmitStyle,
904921
staticMethods: [ExportedFunction] = [],
905922
staticProperties: [ExportedProperty] = [],
906923
documentation: String? = nil
907924
) {
908925
self.name = name
926+
self.jsName = jsName
909927
self.swiftCallName = swiftCallName
910-
self.tsFullPath = tsFullPath
928+
self.tsFullPath =
929+
jsName != nil || jsNamespace != nil
930+
? ((jsNamespace ?? namespace ?? []) + [jsName ?? name]).joined(separator: ".") : tsFullPath
911931
self.explicitAccessControl = explicitAccessControl
912932
self.cases = cases
913933
self.rawType = rawType
914934
self.namespace = namespace
935+
self.jsNamespace = jsNamespace
915936
self.emitStyle = emitStyle
916937
self.staticMethods = staticMethods
917938
self.staticProperties = staticProperties
@@ -947,24 +968,30 @@ public struct ExportedProtocolProperty: Codable, Equatable, Sendable {
947968
}
948969
}
949970

950-
public struct ExportedProtocol: Codable, Equatable {
971+
public struct ExportedProtocol: Codable, Equatable, NamespacedExportedType {
951972
public let name: String
973+
public let jsName: String?
952974
public let methods: [ExportedFunction]
953975
public let properties: [ExportedProtocolProperty]
954976
public let namespace: [String]?
977+
public let jsNamespace: [String]?
955978
public var documentation: String?
956979

957980
public init(
958981
name: String,
982+
jsName: String? = nil,
959983
methods: [ExportedFunction],
960984
properties: [ExportedProtocolProperty] = [],
961985
namespace: [String]? = nil,
986+
jsNamespace: [String]? = nil,
962987
documentation: String? = nil
963988
) {
964989
self.name = name
990+
self.jsName = jsName
965991
self.methods = methods
966992
self.properties = properties
967993
self.namespace = namespace
994+
self.jsNamespace = jsNamespace
968995
self.documentation = documentation
969996
}
970997
}
@@ -1007,35 +1034,41 @@ public struct ExportedFunction: Codable, Equatable, Sendable {
10071034

10081035
public struct ExportedClass: Codable, NamespacedExportedType {
10091036
public var name: String
1037+
public var jsName: String?
10101038
public var swiftCallName: String
10111039
public var explicitAccessControl: String?
10121040
public var constructor: ExportedConstructor?
10131041
public var methods: [ExportedFunction]
10141042
public var properties: [ExportedProperty]
10151043
public var namespace: [String]?
1044+
public var jsNamespace: [String]?
10161045
public var identityMode: Bool? // nil = use config default, true/false = override
10171046
public var documentation: String?
10181047
public var isFinal: Bool?
10191048

10201049
public init(
10211050
name: String,
1051+
jsName: String? = nil,
10221052
swiftCallName: String,
10231053
explicitAccessControl: String?,
10241054
constructor: ExportedConstructor? = nil,
10251055
methods: [ExportedFunction],
10261056
properties: [ExportedProperty] = [],
10271057
namespace: [String]? = nil,
1058+
jsNamespace: [String]? = nil,
10281059
identityMode: Bool? = nil,
10291060
documentation: String? = nil,
10301061
isFinal: Bool? = nil
10311062
) {
10321063
self.name = name
1064+
self.jsName = jsName
10331065
self.swiftCallName = swiftCallName
10341066
self.explicitAccessControl = explicitAccessControl
10351067
self.constructor = constructor
10361068
self.methods = methods
10371069
self.properties = properties
10381070
self.namespace = namespace
1071+
self.jsNamespace = jsNamespace
10391072
self.identityMode = identityMode
10401073
self.documentation = documentation
10411074
self.isFinal = isFinal

Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -697,39 +697,50 @@ import Testing
697697
}
698698

699699
@Test
700-
func jsNameOnClassDiagnostic() throws {
700+
func jsNameOnClassIsAccepted() {
701701
let source = """
702702
@JS("Renamed") class Box { @JS init() {} }
703703
"""
704-
let diagnostics = try #require(moduleDiagnostics(source: source))
705-
#expect(diagnostics.description.contains("A separate name for JavaScript is not supported here"))
704+
#expect(moduleDiagnostics(source: source) == nil)
706705
}
707706

708707
@Test
709-
func jsNameOnStructDiagnostic() throws {
708+
func jsNameOnStructIsAccepted() {
710709
let source = """
711710
@JS("Renamed") struct Box { var x: Int }
712711
"""
713-
let diagnostics = try #require(moduleDiagnostics(source: source))
714-
#expect(diagnostics.description.contains("A separate name for JavaScript is not supported here"))
712+
#expect(moduleDiagnostics(source: source) == nil)
715713
}
716714

717715
@Test
718-
func jsNameOnEnumDiagnostic() throws {
716+
func jsNameOnEnumIsAccepted() {
719717
let source = """
720718
@JS("Renamed") enum Box { case a }
721719
"""
722-
let diagnostics = try #require(moduleDiagnostics(source: source))
723-
#expect(diagnostics.description.contains("A separate name for JavaScript is not supported here"))
720+
#expect(moduleDiagnostics(source: source) == nil)
724721
}
725722

726723
@Test
727-
func jsNameOnProtocolDiagnostic() throws {
724+
func jsNameOnProtocolIsAccepted() {
728725
let source = """
729726
@JS("Renamed") protocol Box { func run() }
730727
"""
731-
let diagnostics = try #require(moduleDiagnostics(source: source))
732-
#expect(diagnostics.description.contains("A separate name for JavaScript is not supported here"))
728+
#expect(moduleDiagnostics(source: source) == nil)
729+
}
730+
731+
@Test(
732+
arguments: ["class Box {}", "struct Box {}", "enum Box { case a }", "protocol Box {}"],
733+
["1invalid", "has space", "has-dash", ""]
734+
)
735+
func invalidTypeJSNameDiagnostic(declaration: String, name: String) throws {
736+
let diagnostics = try #require(moduleDiagnostics(source: "@JS(\"\(name)\") \(declaration)"))
737+
#expect(diagnostics.description.contains("`\(name)` is not a valid JavaScript identifier"))
738+
}
739+
740+
@Test(arguments: ["class Box {}", "struct Box {}", "enum Box { case a }", "protocol Box {}"])
741+
func jsNameOnRepresentationAliasDiagnostic(declaration: String) throws {
742+
let diagnostics = try #require(moduleDiagnostics(source: "@JS(\"PublicBox\", as: String.self) \(declaration)"))
743+
#expect(diagnostics.description.contains("A separate name for JavaScript is not supported on `@JS(as:)` types"))
733744
}
734745

735746
@Test
@@ -741,6 +752,15 @@ import Testing
741752
#expect(diagnostics.description.contains("A separate name for JavaScript is not supported here"))
742753
}
743754

755+
@Test(arguments: ["class", "default", "await", "string", "number"])
756+
func reservedExportedTypeNamesAreRejected(name: String) throws {
757+
let source = """
758+
@JS("\(name)") class Box { @JS init() {} }
759+
"""
760+
let diagnostics = try #require(moduleDiagnostics(source: source))
761+
#expect(diagnostics.description.contains("cannot be used as an exported type name"))
762+
}
763+
744764
@Test
745765
func jsNameOnProtocolRequirementDiagnostic() throws {
746766
let source = """

0 commit comments

Comments
 (0)