Skip to content
Open
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
152 changes: 113 additions & 39 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Large diffs are not rendered by default.

17 changes: 3 additions & 14 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ExternalModuleIndex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,13 @@ public struct ExternalModuleIndex {
}

for klass in exported.classes {
register(dotPath: klass.swiftCallName, bridgeType: .swiftHeapObject(klass.swiftCallName))
register(dotPath: klass.swiftCallName, bridgeType: klass.bridgeType)
}
for structDef in exported.structs {
register(dotPath: structDef.swiftCallName, bridgeType: .swiftStruct(structDef.swiftCallName))
register(dotPath: structDef.swiftCallName, bridgeType: structDef.bridgeType)
}
for enumDef in exported.enums {
let bridgeType: BridgeType
switch enumDef.enumType {
case .simple:
bridgeType = .caseEnum(enumDef.swiftCallName)
case .rawValue:
guard let rawType = enumDef.rawType else { continue }
bridgeType = .rawValueEnum(enumDef.swiftCallName, rawType)
case .associatedValue:
bridgeType = .associatedValueEnum(enumDef.swiftCallName)
case .namespace:
bridgeType = .namespaceEnum(enumDef.swiftCallName)
}
guard let bridgeType = enumDef.bridgeType else { continue }
register(dotPath: enumDef.swiftCallName, bridgeType: bridgeType)
}
for proto in exported.protocols {
Expand Down
4 changes: 2 additions & 2 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -918,7 +918,7 @@ extension BridgeType {
return LoweringParameterInfo(loweredParameters: [("funcRef", .i32)])
case .unsafePointer:
return LoweringParameterInfo(loweredParameters: [("pointer", .pointer)])
case .swiftHeapObject:
case .swiftHeapObject, .swiftBoxedStruct:
return LoweringParameterInfo(loweredParameters: [("pointer", .pointer)])
case .swiftProtocol:
switch context {
Expand Down Expand Up @@ -997,7 +997,7 @@ extension BridgeType {
return LiftingReturnInfo(valueToLift: .i32)
case .unsafePointer:
return LiftingReturnInfo(valueToLift: .pointer)
case .swiftHeapObject:
case .swiftHeapObject, .swiftBoxedStruct:
return LiftingReturnInfo(valueToLift: .pointer)
case .swiftProtocol:
switch context {
Expand Down
129 changes: 86 additions & 43 deletions Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,11 @@ public final class SwiftToSkeleton {
if structDecl.attributes.hasAttribute(name: "JSClass") {
return .jsObject(swiftCallName)
}
if let jsAttribute = structDecl.attributes.firstJSAttribute,
SwiftToSkeleton.extractStructStyle(from: jsAttribute) == .reference
{
return .swiftBoxedStruct(swiftCallName)
}
return .swiftStruct(swiftCallName)
}

Expand Down Expand Up @@ -523,6 +528,22 @@ public final class SwiftToSkeleton {
}
}

static func extractStructStyle(from jsAttribute: AttributeSyntax) -> JSStructStyle? {
guard let arguments = jsAttribute.arguments?.as(LabeledExprListSyntax.self),
let styleArg = arguments.first(where: { $0.label?.text == "structStyle" })
else {
return nil
}
let text = styleArg.expression.trimmedDescription
if text.contains("reference") {
return .reference
}
if text.contains("fields") {
return .fields
}
return nil
}

/// Strips surrounding backticks from an identifier (e.g. "`Foo`" -> "Foo").
static func normalizeIdentifier(_ name: String) -> String {
guard name.hasPrefix("`"), name.hasSuffix("`"), name.count >= 2 else {
Expand Down Expand Up @@ -1397,8 +1418,17 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
let structAbiName = exportedStructByName[structKey]?.abiName ?? "unknown"
staticContext = .structName(structAbiName)
} else {
diagnose(node: node, message: "@JS var must be static in structs (instance fields don't need @JS)")
return .skipChildren
switch exportedStructByName[structKey]?.structStyle ?? .default {
case .reference:
staticContext = nil
case .fields:
diagnose(
node: node,
message: "@JS var must be static in structs (instance fields don't need @JS)",
hint: "Export the struct using @JS(structStyle: .reference) instead"
)
return .skipChildren
}
}
}

Expand Down Expand Up @@ -1661,12 +1691,12 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
for associatedValue in enumCase.associatedValues {
switch associatedValue.type {
case .string, .integer, .float, .double, .bool, .caseEnum, .rawValueEnum,
.swiftStruct, .swiftHeapObject, .jsObject, .associatedValueEnum, .array:
.swiftStruct, .swiftHeapObject, .swiftBoxedStruct, .jsObject, .associatedValueEnum, .array:
break
case .nullable(let wrappedType, _):
switch wrappedType {
case .string, .integer, .float, .double, .bool, .caseEnum, .rawValueEnum,
.swiftStruct, .swiftHeapObject, .jsObject, .associatedValueEnum, .array:
.swiftStruct, .swiftHeapObject, .swiftBoxedStruct, .jsObject, .associatedValueEnum, .array:
break
default:
diagnose(
Expand Down Expand Up @@ -1772,54 +1802,61 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
for: node,
message: "Struct visibility must be at least internal"
)
let structStyle = SwiftToSkeleton.extractStructStyle(from: jsAttribute)

var properties: [ExportedProperty] = []

// Process all variables in struct as readonly (value semantics) and don't require @JS
for member in node.memberBlock.members {
if let varDecl = member.decl.as(VariableDeclSyntax.self) {
let isStatic = varDecl.modifiers.contains { modifier in
modifier.name.tokenKind == .keyword(.static) || modifier.name.tokenKind == .keyword(.class)
}

// Handled with error in visitVariable
if varDecl.attributes.hasJSAttribute() {
continue
}
// Skips static non-@JS properties
if isStatic {
continue
}

for binding in varDecl.bindings {
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else {
continue
switch structStyle ?? .default {
case .reference:
// Reference structs require @JS on variables to export
break
case .fields:
// Process all variables in struct as readonly (value semantics) and don't require @JS
for member in node.memberBlock.members {
if let varDecl = member.decl.as(VariableDeclSyntax.self) {
let isStatic = varDecl.modifiers.contains { modifier in
modifier.name.tokenKind == .keyword(.static) || modifier.name.tokenKind == .keyword(.class)
}

let fieldName = pattern.identifier.text

guard let typeAnnotation = binding.typeAnnotation else {
diagnose(node: binding, message: "Struct field must have explicit type annotation")
// Handled with error in visitVariable
if varDecl.attributes.hasJSAttribute() {
continue
}

guard
let fieldType = withLookupErrors({
self.parent.lookupType(for: typeAnnotation.type, errors: &$0)
})
else {
// Skips static non-@JS properties
if isStatic {
continue
}

let property = ExportedProperty(
name: fieldName,
type: fieldType,
isReadonly: true,
isStatic: false,
namespace: effectiveNamespace,
staticContext: nil
)
properties.append(property)
for binding in varDecl.bindings {
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else {
continue
}

let fieldName = pattern.identifier.text

guard let typeAnnotation = binding.typeAnnotation else {
diagnose(node: binding, message: "Struct field must have explicit type annotation")
continue
}

guard
let fieldType = withLookupErrors({
self.parent.lookupType(for: typeAnnotation.type, errors: &$0)
})
else {
continue
}

let property = ExportedProperty(
name: fieldName,
type: fieldType,
isReadonly: true,
isStatic: false,
namespace: effectiveNamespace,
staticContext: nil
)
properties.append(property)
}
}
}
}
Expand All @@ -1831,7 +1868,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
explicitAccessControl: explicitAccessControl,
properties: properties,
methods: [],
namespace: effectiveNamespace
namespace: effectiveNamespace,
structStyle: structStyle
)

exportedStructByName[structUniqueKey] = exportedStruct
Expand All @@ -1858,6 +1896,11 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
return
}

guard exportedStruct.structStyle ?? .default == .fields else {
// This validation is only required for fields structs.
return
}

let instanceProps = exportedStruct.properties.filter { !$0.isStatic }
let expectedLabels = instanceProps.map(\.name)
let actualLabels = constructor.parameters.compactMap(\.label)
Expand Down
Loading
Loading