Skip to content
Closed
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
5 changes: 5 additions & 0 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public struct ClosureCodegen {

// Generate the call and return value lifting
try builder.call(returnType: signature.returnType)

if signature.isThrows {
builder.body.append("if let error = _swift_js_take_exception() { throw error }")
}

try builder.liftReturnValue(returnType: signature.returnType)

// Get the body code
Expand Down
9 changes: 8 additions & 1 deletion Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1447,7 +1447,7 @@ struct ProtocolCodegen {
let getterBuilder = ImportTS.CallJSEmission(
moduleName: moduleName,
abiName: getterAbiName,
context: .exportSwift
context: property.effects.isThrows ? .exportSwiftProtocol : .exportSwift
)
try getterBuilder.lowerParameter(param: Parameter(label: nil, name: "jsObject", type: .jsObject(nil)))
try getterBuilder.call(returnType: property.type)
Expand All @@ -1466,6 +1466,12 @@ struct ProtocolCodegen {
)

if property.isReadonly {
// Build effect specifiers for getter if property throws
let getterEffects =
property.effects.isThrows || property.effects.isAsync
? ImportTS.buildAccessorEffect(throws: property.effects.isThrows, async: property.effects.isAsync)
: nil

let propertyDecl = VariableDeclSyntax(
bindingSpecifier: .keyword(.var),
bindings: PatternBindingListSyntax {
Expand All @@ -1479,6 +1485,7 @@ struct ProtocolCodegen {
AccessorDeclListSyntax {
AccessorDeclSyntax(
accessorSpecifier: .keyword(.get),
effectSpecifiers: getterEffects,
body: getterBuilder.getBody()
)
}
Expand Down
75 changes: 71 additions & 4 deletions Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,29 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
return Effects(isAsync: isAsync, isThrows: isThrows, isStatic: isStatic)
}

private func collectEffectsFromAccessor(_ accessor: AccessorDeclSyntax) -> Effects? {
let isAsync = accessor.effectSpecifiers?.asyncSpecifier != nil
var isThrows = false
if let throwsClause = accessor.effectSpecifiers?.throwsClause {
guard let thrownType = throwsClause.type else {
diagnose(
node: throwsClause,
message: "Thrown type is not specified, only JSException is supported for now"
)
return nil
}
guard thrownType.trimmedDescription == "JSException" else {
diagnose(
node: throwsClause,
message: "Only JSException is supported for thrown type, got \(thrownType.trimmedDescription)"
)
return nil
}
isThrows = true
}
return Effects(isAsync: isAsync, isThrows: isThrows, isStatic: false)
}

private func extractNamespace(
from jsAttribute: AttributeSyntax
) -> [String]? {
Expand Down Expand Up @@ -1654,18 +1677,62 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
guard let accessorBlock = binding.accessorBlock else {
diagnose(
node: binding,
message: "Protocol property must specify { get } or { get set }",
hint: "Add { get } for readonly or { get set } for readwrite property"
message: "Protocol property must specify { get throws(JSException) }",
hint: "Add { get throws(JSException) } for the property accessor"
)
continue
}

let isReadonly = hasOnlyGetter(accessorBlock)
// Find the getter accessor
let getterAccessor: AccessorDeclSyntax?
switch accessorBlock.accessors {
case .accessors(let accessors):
getterAccessor = accessors.first { $0.accessorSpecifier.tokenKind == .keyword(.get) }
case .getter:
diagnose(
node: accessorBlock,
message: "@JS protocol property getter must declare throws(JSException)",
hint: "Use { get throws(JSException) } syntax"
)
continue
}

guard let getter = getterAccessor else {
diagnose(node: accessorBlock, message: "Protocol property must have a getter")
continue
}

// Check for setter - not allowed with throwing getter
if case .accessors(let accessors) = accessorBlock.accessors {
if accessors.contains(where: { $0.accessorSpecifier.tokenKind == .keyword(.set) }) {
diagnose(
node: accessorBlock,
message: "@JS protocol cannot have { get set } properties",
hint:
"Use readonly property with setter method: var \(propertyName): \(typeAnnotation.type.trimmedDescription) { get throws(JSException) } and func set\(propertyName.capitalized)(_ value: \(typeAnnotation.type.trimmedDescription)) throws(JSException)"
)
continue
}
}

guard let effects = collectEffectsFromAccessor(getter) else {
continue
}

guard effects.isThrows else {
diagnose(
node: getter,
message: "@JS protocol property getter must be throws",
hint: "Declare the getter as 'get throws(JSException)'"
)
continue
}

let exportedProperty = ExportedProtocolProperty(
name: propertyName,
type: propertyType,
isReadonly: isReadonly
isReadonly: true, // Always readonly since { get set } with throws is not allowed
effects: effects
)

if var currentProtocol = exportedProtocolByName[protocolKey] {
Expand Down
28 changes: 27 additions & 1 deletion Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3281,14 +3281,40 @@ extension BridgeJSLink {
for param in method.parameters {
try thunkBuilder.liftParameter(param: param)
}
let returnExpr = try thunkBuilder.callMethod(name: method.name, returnType: method.returnType)

// Check if this is a setter method (setFoo pattern with single parameter and void return)
// If so, convert to property assignment for JavaScript compatibility
let returnExpr: String?
if let propertyName = extractSetterPropertyName(from: method.name),
method.parameters.count == 1,
method.returnType == .void
{
thunkBuilder.callPropertySetter(name: propertyName, returnType: method.returnType)
returnExpr = nil
} else {
returnExpr = try thunkBuilder.callMethod(name: method.name, returnType: method.returnType)
}

let funcLines = thunkBuilder.renderFunction(
name: method.abiName,
returnExpr: returnExpr,
returnType: method.returnType
)
importObjectBuilder.assignToImportObject(name: method.abiName, function: funcLines)
}

/// Extracts property name from a setter method name (e.g., "setFoo" -> "foo")
/// Returns nil if the method name doesn't match the setter pattern
private func extractSetterPropertyName(from methodName: String) -> String? {
guard methodName.hasPrefix("set"), methodName.count > 3 else {
return nil
}
let propertyName = String(methodName.dropFirst(3))
guard !propertyName.isEmpty else {
return nil
}
return propertyName.prefix(1).lowercased() + propertyName.dropFirst()
}
}

/// Utility enum for generating default value representations in JavaScript/TypeScript
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,15 +406,18 @@ public struct ExportedProtocolProperty: Codable, Equatable, Sendable {
public let name: String
public let type: BridgeType
public let isReadonly: Bool
public let effects: Effects

public init(
name: String,
type: BridgeType,
isReadonly: Bool
isReadonly: Bool,
effects: Effects
) {
self.name = name
self.type = type
self.isReadonly = isReadonly
self.effects = effects
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,27 @@ import JavaScriptKit
}

@JS protocol MyViewControllerDelegate {
var eventCount: Int { get set }
var delegateName: String { get }
var optionalName: String? { get set }
var optionalRawEnum: ExampleEnum? { get set }
var rawStringEnum: ExampleEnum { get set }
var result: Result { get set }
var optionalResult: Result? { get set }
var direction: Direction { get set }
var directionOptional: Direction? { get set }
var priority: Priority { get set }
var priorityOptional: Priority? { get set }
var eventCount: Int { get throws(JSException) }
var delegateName: String { get throws(JSException) }
var optionalName: String? { get throws(JSException) }
var optionalRawEnum: ExampleEnum? { get throws(JSException) }
var rawStringEnum: ExampleEnum { get throws(JSException) }
var result: Result { get throws(JSException) }
var optionalResult: Result? { get throws(JSException) }
var direction: Direction { get throws(JSException) }
var directionOptional: Direction? { get throws(JSException) }
var priority: Priority { get throws(JSException) }
var priorityOptional: Priority? { get throws(JSException) }
func setEventCount(_ value: Int) throws(JSException)
func setOptionalName(_ value: String?) throws(JSException)
func setOptionalRawEnum(_ value: ExampleEnum?) throws(JSException)
func setRawStringEnum(_ value: ExampleEnum) throws(JSException)
func setResult(_ value: Result) throws(JSException)
func setOptionalResult(_ value: Result?) throws(JSException)
func setDirection(_ value: Direction) throws(JSException)
func setDirectionOptional(_ value: Direction?) throws(JSException)
func setPriority(_ value: Priority) throws(JSException)
func setPriorityOptional(_ value: Priority?) throws(JSException)
func onSomethingHappened() throws(JSException)
func onValueChanged(_ value: String) throws(JSException)
func onCountUpdated(count: Int) throws(JSException) -> Bool
Expand Down
Loading