Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 76 additions & 80 deletions Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Large diffs are not rendered by default.

200 changes: 124 additions & 76 deletions Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Large diffs are not rendered by default.

45 changes: 32 additions & 13 deletions Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ final class JSGlueVariableScope {
intrinsicRegistry.typeOwnerModules[typeName]
}

func exportedClassPath(forSwiftName name: String) -> [String]? {
intrinsicRegistry.classPaths[name]
}

func renamedEnumNames(forSwiftName name: String) -> (value: String, type: String)? {
intrinsicRegistry.renamedEnumNames[name]
}

func makeChildScope() -> JSGlueVariableScope {
JSGlueVariableScope(intrinsicRegistry: intrinsicRegistry)
}
Expand Down Expand Up @@ -532,8 +540,6 @@ struct IntrinsicJSFragment: Sendable {
/// Whether the fragment has direct access to the SwiftHeapObject classes.
/// If false, the fragment needs to use `_exports` to access the class.
var hasDirectAccessToSwiftClass: Bool = true
/// Maps class names to their namespace path components for resolving `_exports` access.
var classNamespaces: [String: [String]] = [:]

func with<T>(_ keyPath: WritableKeyPath<PrintCodeContext, T>, _ value: T) -> PrintCodeContext {
var new = self
Expand All @@ -545,20 +551,30 @@ struct IntrinsicJSFragment: Sendable {
qualifiedName.split(separator: ".").last.map(String.init) ?? qualifiedName
}

private func exportsAccess(forClass name: String) -> String {
if let namespace = classNamespaces[name], !namespace.isEmpty {
let path = namespace.map { ".\($0)" }.joined()
return "_exports\(path).\(name)"
}
return "_exports['\(name)']"
}

func classReference(forQualifiedName qualifiedName: String) -> String {
if let path = scope.exportedClassPath(forSwiftName: qualifiedName), let name = path.last {
if hasDirectAccessToSwiftClass {
return name
}
return path.count == 1 ? "_exports['\(name)']" : "_exports.\(path.joined(separator: "."))"
}
if hasDirectAccessToSwiftClass {
return unqualifiedClassName(for: qualifiedName)
}
let unqualified = unqualifiedClassName(for: qualifiedName)
return exportsAccess(forClass: unqualified)
return "_exports['\(unqualifiedClassName(for: qualifiedName))']"
}

func defaultValueTypeName(_ type: BridgeType, _ format: DefaultValueUtils.OutputFormat) -> String? {
switch type {
case .swiftHeapObject(let name):
guard let jsName = scope.exportedClassPath(forSwiftName: name)?.last else { return nil }
return format == .javascript ? classReference(forQualifiedName: name) : jsName
case .caseEnum(let name):
guard let renamed = scope.renamedEnumNames(forSwiftName: name) else { return nil }
return format == .javascript ? renamed.value : renamed.type
default:
return nil
}
}
}

Expand Down Expand Up @@ -2565,7 +2581,10 @@ struct IntrinsicJSFragment: Sendable {

// Attach instance methods to the struct instance
for method in structDef.methods where !method.effects.isStatic {
let paramList = DefaultValueUtils.formatParameterList(method.parameters)
let paramList = DefaultValueUtils.formatParameterList(
method.parameters,
resolveTypeName: context.defaultValueTypeName
)
printer.write(
"\(instanceVar).\(method.resolvedJSName) = function(\(paramList)) {"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import BridgeJSUtilities
/// Registry for JS helper intrinsics used during code generation.
final class JSIntrinsicRegistry {
private var entries: [String: [String]] = [:]
var classNamespaces: [String: [String]] = [:]
var classPaths: [String: [String]] = [:]
var renamedEnumNames: [String: (value: String, type: String)] = [:]

var typeOwnerModules: [String: String] = [:]

Expand Down Expand Up @@ -42,7 +43,8 @@ final class JSIntrinsicRegistry {

func reset() {
entries.removeAll()
classNamespaces.removeAll()
classPaths.removeAll()
renamedEnumNames.removeAll()
typeOwnerModules.removeAll()
codecNameOrder.removeAll()
codecBodies.removeAll()
Expand Down
42 changes: 36 additions & 6 deletions Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@

public protocol NamespacedExportedType {
var name: String { get }
var jsName: String? { get }
/// The namespace using Swift declaration names, retained for ABI generation.
var namespace: [String]? { get }
/// The public namespace when an enclosing declaration has an exported name override.
var jsNamespace: [String]? { get }
}

extension NamespacedExportedType {
public var resolvedJSName: String { jsName ?? name }
public var resolvedJSNamespace: [String]? { jsNamespace ?? namespace }

public var abiName: String {
if let namespace = namespace, !namespace.isEmpty {
return (namespace + [name]).joined(separator: "_")
Expand All @@ -16,7 +23,7 @@ extension NamespacedExportedType {
}

public var tsPathComponents: [String] {
(namespace ?? []) + [name]
(resolvedJSNamespace ?? []) + [resolvedJSName]
}

public var tsFullPath: String {
Expand Down Expand Up @@ -778,31 +785,37 @@ public struct StructField: Codable, Equatable, Sendable {

public struct ExportedStruct: Codable, Equatable, Sendable, NamespacedExportedType {
public let name: String
public let jsName: String?
public let swiftCallName: String
public let explicitAccessControl: String?
public var properties: [ExportedProperty]
public var constructor: ExportedConstructor?
public var methods: [ExportedFunction]
public let namespace: [String]?
public let jsNamespace: [String]?
public var documentation: String?

public init(
name: String,
jsName: String? = nil,
swiftCallName: String,
explicitAccessControl: String?,
properties: [ExportedProperty] = [],
constructor: ExportedConstructor? = nil,
methods: [ExportedFunction] = [],
namespace: [String]?,
jsNamespace: [String]? = nil,
documentation: String? = nil
) {
self.name = name
self.jsName = jsName
self.swiftCallName = swiftCallName
self.explicitAccessControl = explicitAccessControl
self.properties = properties
self.constructor = constructor
self.methods = methods
self.namespace = namespace
self.jsNamespace = jsNamespace
self.documentation = documentation
}
}
Expand Down Expand Up @@ -864,12 +877,14 @@ public struct ExportedEnum: Codable, Equatable, Sendable, NamespacedExportedType
public static let objectSuffix = "Object"

public let name: String
public let jsName: String?
public let swiftCallName: String
public let tsFullPath: String
public let explicitAccessControl: String?
public var cases: [EnumCase]
public let rawType: SwiftEnumRawType?
public let namespace: [String]?
public let jsNamespace: [String]?
public let emitStyle: EnumEmitStyle
public var staticMethods: [ExportedFunction]
public var staticProperties: [ExportedProperty] = []
Expand All @@ -885,33 +900,36 @@ public struct ExportedEnum: Codable, Equatable, Sendable, NamespacedExportedType
}

public var valuesName: String {
emitStyle == .tsEnum ? name : "\(name)\(Self.valuesSuffix)"
emitStyle == .tsEnum ? resolvedJSName : "\(resolvedJSName)\(Self.valuesSuffix)"
}

public var objectTypeName: String {
"\(name)\(Self.objectSuffix)"
"\(resolvedJSName)\(Self.objectSuffix)"
}

public init(
name: String,
jsName: String? = nil,
swiftCallName: String,
tsFullPath: String,
explicitAccessControl: String?,
cases: [EnumCase],
rawType: SwiftEnumRawType?,
namespace: [String]?,
jsNamespace: [String]? = nil,
emitStyle: EnumEmitStyle,
staticMethods: [ExportedFunction] = [],
staticProperties: [ExportedProperty] = [],
documentation: String? = nil
) {
self.name = name
self.jsName = jsName
self.swiftCallName = swiftCallName
self.tsFullPath = tsFullPath
self.tsFullPath = ((jsNamespace ?? namespace ?? []) + [jsName ?? name]).joined(separator: ".")
self.explicitAccessControl = explicitAccessControl
self.cases = cases
self.rawType = rawType
self.namespace = namespace
self.jsNamespace = jsNamespace
self.emitStyle = emitStyle
self.staticMethods = staticMethods
self.staticProperties = staticProperties
Expand Down Expand Up @@ -947,24 +965,30 @@ public struct ExportedProtocolProperty: Codable, Equatable, Sendable {
}
}

public struct ExportedProtocol: Codable, Equatable {
public struct ExportedProtocol: Codable, Equatable, NamespacedExportedType {
public let name: String
public let jsName: String?
public let methods: [ExportedFunction]
public let properties: [ExportedProtocolProperty]
public let namespace: [String]?
public let jsNamespace: [String]?
public var documentation: String?

public init(
name: String,
jsName: String? = nil,
methods: [ExportedFunction],
properties: [ExportedProtocolProperty] = [],
namespace: [String]? = nil,
jsNamespace: [String]? = nil,
documentation: String? = nil
) {
self.name = name
self.jsName = jsName
self.methods = methods
self.properties = properties
self.namespace = namespace
self.jsNamespace = jsNamespace
self.documentation = documentation
}
}
Expand Down Expand Up @@ -1007,35 +1031,41 @@ public struct ExportedFunction: Codable, Equatable, Sendable {

public struct ExportedClass: Codable, NamespacedExportedType {
public var name: String
public var jsName: String?
public var swiftCallName: String
public var explicitAccessControl: String?
public var constructor: ExportedConstructor?
public var methods: [ExportedFunction]
public var properties: [ExportedProperty]
public var namespace: [String]?
public var jsNamespace: [String]?
public var identityMode: Bool? // nil = use config default, true/false = override
public var documentation: String?
public var isFinal: Bool?

public init(
name: String,
jsName: String? = nil,
swiftCallName: String,
explicitAccessControl: String?,
constructor: ExportedConstructor? = nil,
methods: [ExportedFunction],
properties: [ExportedProperty] = [],
namespace: [String]? = nil,
jsNamespace: [String]? = nil,
identityMode: Bool? = nil,
documentation: String? = nil,
isFinal: Bool? = nil
) {
self.name = name
self.jsName = jsName
self.swiftCallName = swiftCallName
self.explicitAccessControl = explicitAccessControl
self.constructor = constructor
self.methods = methods
self.properties = properties
self.namespace = namespace
self.jsNamespace = jsNamespace
self.identityMode = identityMode
self.documentation = documentation
self.isFinal = isFinal
Expand Down
36 changes: 0 additions & 36 deletions Plugins/BridgeJS/Tests/BridgeJSToolTests/DiagnosticsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -696,42 +696,6 @@ import Testing
}
}

@Test
func jsNameOnClassDiagnostic() throws {
let source = """
@JS("Renamed") class Box { @JS init() {} }
"""
let diagnostics = try #require(moduleDiagnostics(source: source))
#expect(diagnostics.description.contains("A separate name for JavaScript is not supported here"))
}

@Test
func jsNameOnStructDiagnostic() throws {
let source = """
@JS("Renamed") struct Box { var x: Int }
"""
let diagnostics = try #require(moduleDiagnostics(source: source))
#expect(diagnostics.description.contains("A separate name for JavaScript is not supported here"))
}

@Test
func jsNameOnEnumDiagnostic() throws {
let source = """
@JS("Renamed") enum Box { case a }
"""
let diagnostics = try #require(moduleDiagnostics(source: source))
#expect(diagnostics.description.contains("A separate name for JavaScript is not supported here"))
}

@Test
func jsNameOnProtocolDiagnostic() throws {
let source = """
@JS("Renamed") protocol Box { func run() }
"""
let diagnostics = try #require(moduleDiagnostics(source: source))
#expect(diagnostics.description.contains("A separate name for JavaScript is not supported here"))
}

@Test
func jsNameOnInitializerDiagnostic() throws {
let source = """
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation
import SwiftParser
import Testing

@testable import BridgeJSCore
@testable import BridgeJSSkeleton

@Suite struct ExportedTypeNameTests {
private func parse(_ source: String) throws -> ExportedSkeleton {
let generator = SwiftToSkeleton(
progress: .silent,
moduleName: "TestModule",
exposeToGlobal: false,
externalModuleIndex: .empty
)
generator.addSourceFile(Parser.parse(source: source), inputFilePath: "Types.swift")
return try #require(generator.finalize().exported)
}

@Test
func renamedTypesKeepSwiftIdentities() throws {
let exported = try parse(
"""
@JS func box() -> SwiftBox
@JS func record() -> SwiftRecord
@JS func choice() -> SwiftChoice
@JS func delegate(_ value: SwiftDelegate)
@JS("PublicBox") class SwiftBox { @JS init() {} }
@JS("PublicRecord") struct SwiftRecord { var value: Int }
@JS("PublicChoice") enum SwiftChoice { case first }
@JS("PublicDelegate") protocol SwiftDelegate { func run() }
"""
)
#expect(exported.classes.first?.resolvedJSName == "PublicBox")
#expect(exported.structs.first?.resolvedJSName == "PublicRecord")
#expect(exported.enums.first?.resolvedJSName == "PublicChoice")
#expect(exported.protocols.first?.resolvedJSName == "PublicDelegate")
#expect(exported.functions[0].returnType == .swiftHeapObject("SwiftBox"))
#expect(exported.functions[1].returnType == .swiftStruct("SwiftRecord"))
#expect(exported.functions[2].returnType == .caseEnum("SwiftChoice"))
#expect(exported.functions[3].parameters.first?.type == .swiftProtocol("SwiftDelegate"))
}

@Test
func renamedParentsKeepSwiftThunkABI() throws {
let source = """
@JS(namespace: "API") enum InternalAPI {
@JS class Item { @JS init() {} }
}
"""
let original = try parse(source)
let renamed = try parse(
source.replacingOccurrences(of: "@JS(namespace:", with: "@JS(\"PublicAPI\", namespace:")
)
let originalThunks = try ExportSwift(progress: .silent, moduleName: "TestModule", skeleton: original).finalize()
let renamedThunks = try ExportSwift(progress: .silent, moduleName: "TestModule", skeleton: renamed).finalize()
#expect(renamedThunks == originalThunks)
#expect(renamed.classes.first?.tsFullPath == "API.PublicAPI.Item")
}
}
Loading
Loading