Skip to content

Commit 648566b

Browse files
committed
[BridgeJS] Synthesize typed-closure init access from declaration surface
Resolves #709: a public `@JSClass` exposing a `JSTypedClosure<...>` parameter could not be consumed from another target because the synthesized `extension JSTypedClosure { init(...) }` was always internal, leaving downstream callers no way to construct the closure value without hand-rolling a public wrapper. Imported skeleton entries now record the source access level (`public`/`package`/`internal`); the closure-signature collector takes the maximum across every surface that references a given signature, and `ClosureCodegen` prefixes the synthesized init with the resulting modifier (internal stays bare). This matches the pattern `JSClassMacro` already uses for `init(unsafelyWrapping:)`.
1 parent f3ce20c commit 648566b

32 files changed

Lines changed: 1333 additions & 72 deletions

Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ public struct ClosureCodegen {
2121
return "(\(closureParams))\(swiftEffects) -> \(swiftReturnType)"
2222
}
2323

24-
func renderClosureHelpers(_ signature: ClosureSignature) throws -> [DeclSyntax] {
24+
func renderClosureHelpers(
25+
_ signature: ClosureSignature,
26+
accessLevel: BridgeJSAccessLevel = .internal
27+
) throws -> [DeclSyntax] {
2528
let mangledName = signature.mangleName
2629
let helperName = "_BJS_Closure_\(mangledName)"
2730
let swiftClosureType = swiftClosureType(for: signature)
@@ -99,9 +102,10 @@ public struct ClosureCodegen {
99102

100103
let helperEnumDecl: DeclSyntax = "\(raw: helperEnumDeclPrinter.lines.joined(separator: "\n"))"
101104

105+
let initAccessModifier = accessLevel.modifierKeyword.map { "\($0) " } ?? ""
102106
let typedClosureExtension: DeclSyntax = """
103107
extension JSTypedClosure where Signature == \(raw: swiftClosureType) {
104-
init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping \(raw: swiftClosureType)) {
108+
\(raw: initAccessModifier)init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping \(raw: swiftClosureType)) {
105109
self.init(
106110
makeClosure: \(raw: externABIName),
107111
body: body,
@@ -192,12 +196,13 @@ public struct ClosureCodegen {
192196
let collector = ClosureSignatureCollectorVisitor(moduleName: skeleton.moduleName)
193197
var walker = BridgeSkeletonWalker(visitor: collector)
194198
walker.walk(skeleton)
195-
let closureSignatures = walker.visitor.signatures
196-
guard !closureSignatures.isEmpty else { return nil }
199+
let signatureAccessLevels = walker.visitor.signatureAccessLevels
200+
guard !signatureAccessLevels.isEmpty else { return nil }
197201

198202
var decls: [DeclSyntax] = []
199-
for signature in closureSignatures.sorted(by: { $0.mangleName < $1.mangleName }) {
200-
decls.append(contentsOf: try renderClosureHelpers(signature))
203+
for signature in signatureAccessLevels.keys.sorted(by: { $0.mangleName < $1.mangleName }) {
204+
let accessLevel = signatureAccessLevels[signature] ?? .internal
205+
decls.append(contentsOf: try renderClosureHelpers(signature, accessLevel: accessLevel))
201206
decls.append(try renderClosureInvokeHandler(signature))
202207
}
203208

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2056,6 +2056,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
20562056
let name: String
20572057
let jsName: String?
20582058
let from: JSImportFrom?
2059+
let accessLevel: BridgeJSAccessLevel
20592060
var constructor: ImportedConstructorSkeleton?
20602061
var methods: [ImportedFunctionSkeleton]
20612062
var staticMethods: [ImportedFunctionSkeleton]
@@ -2271,6 +2272,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
22712272
name: typeName,
22722273
jsName: nil,
22732274
from: nil,
2275+
accessLevel: .internal,
22742276
constructor: nil,
22752277
methods: [],
22762278
staticMethods: [],
@@ -2279,12 +2281,18 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
22792281
)
22802282
}
22812283

2282-
private func enterJSClass(_ typeName: String, jsName: String?, from: JSImportFrom?) {
2284+
private func enterJSClass(
2285+
_ typeName: String,
2286+
jsName: String?,
2287+
from: JSImportFrom?,
2288+
accessLevel: BridgeJSAccessLevel
2289+
) {
22832290
stateStack.append(.jsClassBody(name: typeName))
22842291
currentType = CurrentType(
22852292
name: typeName,
22862293
jsName: jsName,
22872294
from: from,
2295+
accessLevel: accessLevel,
22882296
constructor: nil,
22892297
methods: [],
22902298
staticMethods: [],
@@ -2305,7 +2313,8 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
23052313
staticMethods: type.staticMethods,
23062314
getters: type.getters,
23072315
setters: type.setters,
2308-
documentation: nil
2316+
documentation: nil,
2317+
accessLevel: type.accessLevel
23092318
)
23102319
)
23112320
currentType = nil
@@ -2318,7 +2327,8 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
23182327
let attribute = AttributeChecker.firstJSClassAttribute(node.attributes)
23192328
let jsName = attribute.flatMap(AttributeChecker.extractJSName)
23202329
let from = attribute.flatMap(AttributeChecker.extractJSImportFrom)
2321-
enterJSClass(node.name.text, jsName: jsName, from: from)
2330+
let accessLevel = Self.bridgeAccessLevel(from: node.modifiers)
2331+
enterJSClass(node.name.text, jsName: jsName, from: from, accessLevel: accessLevel)
23222332
}
23232333
return .visitChildren
23242334
}
@@ -2334,7 +2344,8 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
23342344
let attribute = AttributeChecker.firstJSClassAttribute(node.attributes)
23352345
let jsName = attribute.flatMap(AttributeChecker.extractJSName)
23362346
let from = attribute.flatMap(AttributeChecker.extractJSImportFrom)
2337-
enterJSClass(node.name.text, jsName: jsName, from: from)
2347+
let accessLevel = Self.bridgeAccessLevel(from: node.modifiers)
2348+
enterJSClass(node.name.text, jsName: jsName, from: from, accessLevel: accessLevel)
23382349
}
23392350
return .visitChildren
23402351
}
@@ -2499,8 +2510,14 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
24992510
else {
25002511
return nil
25012512
}
2513+
// Initializers without an explicit modifier inherit access from the
2514+
// enclosing `@JSClass` (the user's example pattern: `public init(...)`
2515+
// inside `public struct JSDocument`).
2516+
let parentLevel = currentType?.accessLevel ?? .internal
2517+
let accessLevel = Self.bridgeAccessLevel(from: initializer.modifiers, default: parentLevel)
25022518
return ImportedConstructorSkeleton(
2503-
parameters: parseParameters(from: initializer.signature.parameterClause)
2519+
parameters: parseParameters(from: initializer.signature.parameterClause),
2520+
accessLevel: accessLevel
25042521
)
25052522
}
25062523

@@ -2533,14 +2550,16 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
25332550
} else {
25342551
returnType = .void
25352552
}
2553+
let accessLevel = Self.bridgeAccessLevel(from: node.modifiers)
25362554
return ImportedFunctionSkeleton(
25372555
name: name,
25382556
jsName: jsName,
25392557
from: from,
25402558
parameters: parameters,
25412559
returnType: returnType,
25422560
effects: effects,
2543-
documentation: nil
2561+
documentation: nil,
2562+
accessLevel: accessLevel
25442563
)
25452564
}
25462565

@@ -2572,13 +2591,15 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
25722591
let propertyName = SwiftToSkeleton.normalizeIdentifier(identifier.identifier.text)
25732592
let jsName = AttributeChecker.extractJSName(from: jsGetter)
25742593
let from = AttributeChecker.extractJSImportFrom(from: jsGetter)
2594+
let accessLevel = Self.bridgeAccessLevel(from: node.modifiers)
25752595
return ImportedGetterSkeleton(
25762596
name: propertyName,
25772597
jsName: jsName,
25782598
from: from,
25792599
type: propertyType,
25802600
documentation: nil,
2581-
functionName: nil
2601+
functionName: nil,
2602+
accessLevel: accessLevel
25822603
)
25832604
}
25842605

@@ -2601,12 +2622,14 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
26012622
return nil
26022623
}
26032624

2625+
let accessLevel = Self.bridgeAccessLevel(from: node.modifiers)
26042626
return ImportedSetterSkeleton(
26052627
name: propertyName,
26062628
jsName: validation.jsName,
26072629
type: validation.valueType,
26082630
documentation: nil,
2609-
functionName: "\(functionBaseName)_set"
2631+
functionName: "\(functionBaseName)_set",
2632+
accessLevel: accessLevel
26102633
)
26112634
}
26122635

@@ -2652,6 +2675,31 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
26522675
modifier.name.tokenKind == .keyword(.static) || modifier.name.tokenKind == .keyword(.class)
26532676
}
26542677
}
2678+
2679+
/// Maps Swift's declaration modifiers to a `BridgeJSAccessLevel` for
2680+
/// recording on imported skeleton entries. Falls back to `default` when no
2681+
/// access modifier is present (typically `.internal`, but the caller may
2682+
/// override — e.g. an `init` inheriting from its enclosing `@JSClass`).
2683+
/// `private`/`fileprivate` are mapped to the fallback because the macros
2684+
/// already reject those access levels for `@JS*` declarations.
2685+
fileprivate static func bridgeAccessLevel(
2686+
from modifiers: DeclModifierListSyntax,
2687+
default fallback: BridgeJSAccessLevel = .internal
2688+
) -> BridgeJSAccessLevel {
2689+
for modifier in modifiers {
2690+
switch modifier.name.tokenKind {
2691+
case .keyword(.public), .keyword(.open):
2692+
return .public
2693+
case .keyword(.package):
2694+
return .package
2695+
case .keyword(.internal):
2696+
return .internal
2697+
default:
2698+
continue
2699+
}
2700+
}
2701+
return fallback
2702+
}
26552703
}
26562704

26572705
extension GenericArgumentListSyntax {

0 commit comments

Comments
 (0)