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
8 changes: 4 additions & 4 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,15 @@ public struct ClosureCodegen {
} else {
printer.write("let result = \(closureCallExpr)")
switch signature.returnType {
case .swiftProtocol:
case .swiftProtocol(let protocolName):
printer.write(
"return (result as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()"
"return _bridgeJSUnwrapProtocolExportable(result, \"\(protocolName)\").bridgeJSLowerAsProtocolReturn()"
)
case .nullable(.swiftProtocol, _):
case .nullable(.swiftProtocol(let protocolName), _):
printer.write("if let result {")
printer.indent {
printer.write(
"_swift_js_return_optional_object(1, (result as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn())"
"_swift_js_return_optional_object(1, _bridgeJSUnwrapProtocolExportable(result, \"\(protocolName)\").bridgeJSLowerAsProtocolReturn())"
)
}
printer.write("} else {")
Expand Down
30 changes: 18 additions & 12 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,8 @@ public class ExportSwift {

private func protocolCastSuffix(for returnType: BridgeType) -> (prefix: String, suffix: String) {
switch returnType {
case .swiftProtocol:
return ("", " as! _BridgedSwiftProtocolExportable")
case .swiftProtocol(let name):
return ("_bridgeJSUnwrapProtocolExportable(", ", \"\(name)\")")
default:
return ("", "")
}
Expand Down Expand Up @@ -424,11 +424,11 @@ public class ExportSwift {
}
case .swiftProtocol:
append("return ret.bridgeJSLowerAsProtocolReturn()")
case .nullable(.swiftProtocol, _):
case .nullable(.swiftProtocol(let protocolName), _):
append(
"""
if let ret {
_swift_js_return_optional_object(1, (ret as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn())
_swift_js_return_optional_object(1, _bridgeJSUnwrapProtocolExportable(ret, "\(raw: protocolName)").bridgeJSLowerAsProtocolReturn())
} else {
_swift_js_return_optional_object(0, 0)
}
Expand Down Expand Up @@ -993,9 +993,9 @@ struct StackCodegen {
return ["\(raw: accessor).bridgeJSStackPush()"]
case .jsObject(_?):
return ["\(raw: accessor).jsObject.bridgeJSStackPush()"]
case .swiftProtocol:
case .swiftProtocol(let protocolName):
return [
"_swift_js_push_i32((\(raw: accessor) as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn())"
"_swift_js_push_i32(_bridgeJSUnwrapProtocolExportable(\(raw: accessor), \"\(raw: protocolName)\").bridgeJSLowerAsProtocolReturn())"
]
case .void, .namespaceEnum:
return []
Expand All @@ -1016,8 +1016,8 @@ struct StackCodegen {
varPrefix: String
) -> [CodeBlockItemSyntax] {
switch elementType {
case .swiftProtocol:
return lowerProtocolArrayStatements(accessor: accessor, varPrefix: varPrefix)
case .swiftProtocol(let protocolName):
return lowerProtocolArrayStatements(protocolName: protocolName, accessor: accessor, varPrefix: varPrefix)
case .void, .namespaceEnum:
fatalError("Invalid array element type: \(elementType)")
default:
Expand All @@ -1026,14 +1026,15 @@ struct StackCodegen {
}

private func lowerProtocolArrayStatements(
protocolName: String,
accessor: String,
varPrefix: String
) -> [CodeBlockItemSyntax] {
let elemVar = "__bjs_elem_\(varPrefix)"
return [
"""
for \(raw: elemVar) in \(raw: accessor) {
_swift_js_push_i32((\(raw: elemVar) as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn())
_swift_js_push_i32(_bridgeJSUnwrapProtocolExportable(\(raw: elemVar), "\(raw: protocolName)").bridgeJSLowerAsProtocolReturn())
}
""",
"_swift_js_push_i32(Int32(\(raw: accessor).count))",
Expand All @@ -1048,8 +1049,12 @@ struct StackCodegen {
switch valueType {
case .jsObject(let className?) where className != "JSObject":
return ["\(raw: accessor).mapValues { $0.jsObject }.bridgeJSStackPush()"]
case .swiftProtocol:
return lowerProtocolDictionaryStatements(accessor: accessor, varPrefix: varPrefix)
case .swiftProtocol(let protocolName):
return lowerProtocolDictionaryStatements(
protocolName: protocolName,
accessor: accessor,
varPrefix: varPrefix
)
case .nullable, .closure:
return lowerDictionaryStatementsInline(
valueType: valueType,
Expand Down Expand Up @@ -1107,6 +1112,7 @@ struct StackCodegen {
}

private func lowerProtocolDictionaryStatements(
protocolName: String,
accessor: String,
varPrefix: String
) -> [CodeBlockItemSyntax] {
Expand All @@ -1115,7 +1121,7 @@ struct StackCodegen {
"""
for \(raw: pairVar) in \(raw: accessor) {
\(raw: pairVar).key.bridgeJSStackPush()
_swift_js_push_i32((\(raw: pairVar).value as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn())
_swift_js_push_i32(_bridgeJSUnwrapProtocolExportable(\(raw: pairVar).value, "\(raw: protocolName)").bridgeJSLowerAsProtocolReturn())
}
""",
"_swift_js_push_i32(Int32(\(raw: accessor).count))",
Expand Down
8 changes: 4 additions & 4 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,12 @@ public struct ImportTS {
}
)
)
} else if case .nullable(.swiftProtocol, _) = param.type, context == .exportSwift {
} else if case .nullable(.swiftProtocol(let protocolName), _) = param.type, context == .exportSwift {
body.write("let \(pattern): (Int32, Int32)")
body.write("if let \(param.name) {")
body.indent {
body.write(
"\(pattern) = (1, (\(param.name) as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn())"
"\(pattern) = (1, _bridgeJSUnwrapProtocolExportable(\(param.name), \"\(protocolName)\").bridgeJSLowerAsProtocolReturn())"
)
}
body.write("} else {")
Expand All @@ -203,9 +203,9 @@ public struct ImportTS {
body.write("}")
} else {
let initializerExpr: ExprSyntax
if case .swiftProtocol = param.type, context == .exportSwift {
if case .swiftProtocol(let protocolName) = param.type, context == .exportSwift {
initializerExpr = ExprSyntax(
"(\(raw: param.name) as! _BridgedSwiftProtocolExportable).bridgeJSLowerAsProtocolReturn()"
"_bridgeJSUnwrapProtocolExportable(\(raw: param.name), \"\(raw: protocolName)\").bridgeJSLowerAsProtocolReturn()"
)
} else {
initializerExpr = ExprSyntax("\(raw: param.name).bridgeJSLowerParameter()")
Expand Down
95 changes: 95 additions & 0 deletions Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ public final class SwiftToSkeleton {
collector.finalize(&exported)
}

perSourceErrors.append(contentsOf: diagnoseProtocolConformances(in: exported))

if !perSourceErrors.isEmpty {
let diagnostics = perSourceErrors.flatMap { inputFilePath, errors in
errors.map { (file: inputFilePath, diagnostic: $0) }
Expand All @@ -318,6 +320,99 @@ public final class SwiftToSkeleton {
)
}

private func diagnoseProtocolConformances(
in exported: ExportedSkeleton
) -> [(inputFilePath: String, errors: [DiagnosticError])] {
var loweredProtocols: Set<String> = []
func collect(_ type: BridgeType, loweredBySwift: Bool) {
switch type {
case .swiftProtocol(let name):
if loweredBySwift { loweredProtocols.insert(name) }
case .array(let element), .dictionary(let element), .nullable(let element, _), .alias(_, let element):
collect(element, loweredBySwift: loweredBySwift)
case .closure(let signature, _):
for parameter in signature.parameters {
collect(parameter, loweredBySwift: !loweredBySwift)
}
collect(signature.returnType, loweredBySwift: loweredBySwift)
default:
break
}
}
func collect(_ function: ExportedFunction, loweredReturn: Bool = true) {
for parameter in function.parameters { collect(parameter.type, loweredBySwift: !loweredReturn) }
collect(function.returnType, loweredBySwift: loweredReturn)
}

for function in exported.functions + exported.classes.flatMap(\.methods)
+ exported.structs.flatMap(\.methods) + exported.enums.flatMap(\.staticMethods)
{
collect(function)
}
for constructor in exported.classes.compactMap(\.constructor) + exported.structs.compactMap(\.constructor) {
for parameter in constructor.parameters { collect(parameter.type, loweredBySwift: false) }
}
for property in exported.classes.flatMap(\.properties) + exported.enums.flatMap(\.staticProperties) {
collect(property.type, loweredBySwift: true)
if !property.isReadonly { collect(property.type, loweredBySwift: false) }
}
for property in exported.structs.flatMap(\.properties) {
collect(property.type, loweredBySwift: true)
if !property.isStatic || !property.isReadonly { collect(property.type, loweredBySwift: false) }
}
for value in exported.enums.flatMap(\.cases).flatMap(\.associatedValues) {
collect(value.type, loweredBySwift: true)
}
for protocolDef in exported.protocols {
for method in protocolDef.methods { collect(method, loweredReturn: false) }
for property in protocolDef.properties {
collect(property.type, loweredBySwift: false)
if !property.isReadonly { collect(property.type, loweredBySwift: true) }
}
}

guard !loweredProtocols.isEmpty else { return [] }
var diagnostics: [(inputFilePath: String, errors: [DiagnosticError])] = []
for declaration in typeDeclResolver.declarationsWithInheritance where !declaration.is(ProtocolDeclSyntax.self) {
let extendedType = declaration.as(ExtensionDeclSyntax.self)?.extendedType
let target = extendedType.flatMap { typeDeclResolver.resolveExtensionTarget($0) }
let classDecl = declaration.as(ClassDeclSyntax.self) ?? target?.as(ClassDeclSyntax.self)
if classDecl?.attributes.hasJSAttribute() == true { continue }
if let extendedType, target == nil {
var errors: [DiagnosticError] = []
if case .swiftHeapObject = resolveExternal(for: extendedType, errors: &errors) { continue }
}
guard
let name = declaration.asProtocol(NamedDeclSyntax.self)?.name.text ?? extendedType?.trimmedDescription,
let inputFilePath = sourceFiles.first(where: { $0.sourceFile.id == declaration.root.id })?.inputFilePath
else { continue }
for inherited in declaration.inheritanceClause?.inheritedTypes ?? [] {
guard let protocolDecl = typeDeclResolver.resolve(inherited.type)?.as(ProtocolDeclSyntax.self),
protocolDecl.attributes.hasJSAttribute(), loweredProtocols.contains(protocolDecl.name.text)
else { continue }
let protocolName = protocolDecl.name.text
diagnostics.append(
(
inputFilePath,
[
DiagnosticError(
node: declaration,
message:
"'\(name)' conforms to '\(protocolName)', a @JS protocol that exported APIs "
+ "bridge to JavaScript, but '\(name)' is not a '@JS class'. Passing it to "
+ "JavaScript as 'any \(protocolName)' would trap at runtime.",
hint:
"Mark '\(name)' as a '@JS class' so it can cross the bridge, or avoid using "
+ "'\(protocolName)' as an existential in exported APIs."
)
]
)
)
}
}
return diagnostics
}

private static let jsTypedArrayTypealiasNames: [String: String] = [
"Int8": "JSInt8Array",
"UInt8": "JSUint8Array",
Expand Down
7 changes: 7 additions & 0 deletions Plugins/BridgeJS/Sources/BridgeJSCore/TypeDeclResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class TypeDeclResolver {
typealias QualifiedName = [String]
private var typeDeclByQualifiedName: [QualifiedName: TypeDecl] = [:]
private var typeAliasByQualifiedName: [QualifiedName: TypeAliasDeclSyntax] = [:]
private(set) var declarationsWithInheritance: [any DeclGroupSyntax] = []

enum Error: Swift.Error {
case typeNotFound(QualifiedName)
Expand All @@ -24,6 +25,9 @@ class TypeDeclResolver {
}

func visitNominalDecl(_ node: TypeDecl) -> SyntaxVisitorContinueKind {
if node.inheritanceClause != nil {
resolver.declarationsWithInheritance.append(node)
}
let name = node.name.text
let qualifiedName = scope + [name]
resolver.typeDeclByQualifiedName[qualifiedName] = node
Expand Down Expand Up @@ -74,6 +78,9 @@ class TypeDeclResolver {
}

override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
if node.inheritanceClause != nil {
resolver.declarationsWithInheritance.append(node)
}
guard let components = node.memberScopeComponents else {
return .skipChildren
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,27 @@ import Testing
}
}

@Test func exportedClassConformanceInAnotherModule() throws {
let core = try makeSkeleton(
"""
@JS public class MyImpl {
@JS public init() {}
@JS public func ok() -> Int { 42 }
}
""",
moduleName: "Core"
)
_ = try makeSkeleton(
"""
import Core
@JS protocol P { func ok() -> Int }
extension MyImpl: P {}
@JS func get() -> P { MyImpl() }
""",
dependencies: [(moduleName: "Core", skeleton: core)]
)
}

// MARK: - Utillites

private func resolveApp(
Expand Down
Loading
Loading