@@ -300,6 +300,8 @@ public final class SwiftToSkeleton {
300300 collector. finalize ( & exported)
301301 }
302302
303+ mergeExtensionDeclaredJSProtocolConformances ( into: & exported)
304+
303305 perSourceErrors. append ( contentsOf: diagnoseProtocolConformances ( in: exported) )
304306
305307 if !perSourceErrors. isEmpty {
@@ -418,6 +420,50 @@ public final class SwiftToSkeleton {
418420 return diagnostics
419421 }
420422
423+ /// Folds `extension Foo: SomeJSProtocol {}` conformances into the exported
424+ /// skeleton's `conformedJSProtocols` lists.
425+ ///
426+ /// The per-declaration collectors only see the type's own inheritance
427+ /// clause, but declaring the conformance in an extension is common Swift
428+ /// style, and the JS-side generic constraint check
429+ /// (`__bjs_tokenConformances`) must not reject a type that genuinely
430+ /// conforms.
431+ private func mergeExtensionDeclaredJSProtocolConformances( into exported: inout ExportedSkeleton ) {
432+ var protocolsByTarget : [ String : [ String ] ] = [ : ]
433+ for (sourceFile, _) in sourceFiles {
434+ let collector = ExtensionConformanceCollector ( parent: self )
435+ collector. walk ( sourceFile)
436+ for (target, protocols) in collector. protocolsByTarget {
437+ protocolsByTarget [ target, default: [ ] ] . append ( contentsOf: protocols)
438+ }
439+ }
440+ guard !protocolsByTarget. isEmpty else { return }
441+
442+ func merge( _ existing: inout [ String ] ? , additions: [ String ] ) {
443+ var result = existing ?? [ ]
444+ for name in additions where !result. contains ( name) {
445+ result. append ( name)
446+ }
447+ existing = result. isEmpty ? nil : result
448+ }
449+
450+ for index in exported. structs. indices {
451+ if let additions = protocolsByTarget [ exported. structs [ index] . swiftCallName] {
452+ merge ( & exported. structs [ index] . conformedJSProtocols, additions: additions)
453+ }
454+ }
455+ for index in exported. classes. indices {
456+ if let additions = protocolsByTarget [ exported. classes [ index] . swiftCallName] {
457+ merge ( & exported. classes [ index] . conformedJSProtocols, additions: additions)
458+ }
459+ }
460+ for index in exported. enums. indices {
461+ if let additions = protocolsByTarget [ exported. enums [ index] . swiftCallName] {
462+ merge ( & exported. enums [ index] . conformedJSProtocols, additions: additions)
463+ }
464+ }
465+ }
466+
421467 private static let jsTypedArrayTypealiasNames : [ String : String ] = [
422468 " Int8 " : " JSInt8Array " ,
423469 " UInt8 " : " JSUint8Array " ,
@@ -1575,6 +1621,20 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
15751621 }
15761622 }
15771623
1624+ /// Names of the `@JS protocol`s a type's inheritance clause declares
1625+ /// conformance to. Non-protocol entries (raw types, plain Swift protocols)
1626+ /// are ignored.
1627+ private func collectConformedJSProtocols( from inheritanceClause: InheritanceClauseSyntax ? ) -> [ String ] ? {
1628+ guard let inheritanceClause else { return nil }
1629+ var names : [ String ] = [ ]
1630+ for inherited in inheritanceClause. inheritedTypes {
1631+ if let name = parent. resolveJSProtocolConstraint ( for: inherited. type) , !names. contains ( name) {
1632+ names. append ( name)
1633+ }
1634+ }
1635+ return names. isEmpty ? nil : names
1636+ }
1637+
15781638 /// Shared parameter parsing logic used by functions, initializers, and protocol methods
15791639 private func parseParameters(
15801640 from parameterClause: FunctionParameterClauseSyntax ,
@@ -1682,19 +1742,16 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
16821742 return nil
16831743 }
16841744
1685- var genericParameterNames : [ String ] = [ ]
1745+ var genericParameters : [ GenericParameter ] = [ ]
16861746 if let genericClause = node. genericParameterClause {
16871747 for genericParam in genericClause. parameters {
1688- let constraint = genericParam. inheritedType? . trimmedDescription ?? " "
1689- guard SwiftToSkeleton . isBridgeableGenericConstraint ( constraint) else {
1690- diagnose (
1691- node: node,
1692- message:
1693- " Generic parameter ' \( genericParam. name. text) ' must be constrained to 'BridgedSwiftGenericBridgeable' to be used with @JS. "
1694- )
1748+ switch parent. parseGenericParameterConstraints ( genericParam, attributeName: " @JS " ) {
1749+ case . failed( let message) :
1750+ diagnose ( node: node, message: message)
16951751 return nil
1752+ case . parsed( let genericParameter) :
1753+ genericParameters. append ( genericParameter)
16961754 }
1697- genericParameterNames. append ( genericParam. name. text)
16981755 }
16991756 if node. genericWhereClause != nil {
17001757 diagnose (
@@ -1719,6 +1776,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
17191776 }
17201777 }
17211778
1779+ let genericParameterNames = genericParameters. map ( \. name)
1780+
17221781 let name = node. name. text
17231782 let jsName = extractValidatedJSName ( from: jsAttribute)
17241783
@@ -1883,7 +1942,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
18831942 namespace: finalNamespace,
18841943 staticContext: staticContext,
18851944 documentation: extractDocumentation ( from: node) ,
1886- genericParameters: genericParameterNames . isEmpty ? nil : genericParameterNames
1945+ genericParameters: genericParameters . isEmpty ? nil : genericParameters
18871946 )
18881947 }
18891948
@@ -2275,7 +2334,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
22752334 jsNamespace: namespaceResult. jsNamespace,
22762335 identityMode: classIdentityMode,
22772336 documentation: extractDocumentation ( from: node) ,
2278- isFinal: isFinal
2337+ isFinal: isFinal,
2338+ conformedJSProtocols: collectConformedJSProtocols ( from: node. inheritanceClause)
22792339 )
22802340 let uniqueKey = makeKey ( name: name, namespace: effectiveNamespace)
22812341
@@ -2440,7 +2500,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
24402500 emitStyle: emitStyle,
24412501 staticMethods: [ ] ,
24422502 staticProperties: [ ] ,
2443- documentation: extractDocumentation ( from: node)
2503+ documentation: extractDocumentation ( from: node) ,
2504+ conformedJSProtocols: collectConformedJSProtocols ( from: node. inheritanceClause)
24442505 )
24452506
24462507 let enumUniqueKey = makeKey ( name: name, namespace: effectiveNamespace)
@@ -2693,7 +2754,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
26932754 methods: [ ] ,
26942755 namespace: effectiveNamespace,
26952756 jsNamespace: namespaceResult. jsNamespace,
2696- documentation: extractDocumentation ( from: node)
2757+ documentation: extractDocumentation ( from: node) ,
2758+ conformedJSProtocols: collectConformedJSProtocols ( from: node. inheritanceClause)
26972759 )
26982760
26992761 exportedStructByName [ structUniqueKey] = exportedStruct
@@ -4076,3 +4138,27 @@ extension GenericArgumentListSyntax {
40764138 #endif
40774139 }
40784140}
4141+
4142+ /// Collects `@JS protocol` conformances declared through extensions, keyed by
4143+ /// the extended type's (possibly qualified) name as written in source, which
4144+ /// matches the skeleton's `swiftCallName` for both top-level and nested types.
4145+ private final class ExtensionConformanceCollector : SyntaxVisitor {
4146+ private let parent : SwiftToSkeleton
4147+ var protocolsByTarget : [ String : [ String ] ] = [ : ]
4148+
4149+ init ( parent: SwiftToSkeleton ) {
4150+ self . parent = parent
4151+ super. init ( viewMode: . sourceAccurate)
4152+ }
4153+
4154+ override func visit( _ node: ExtensionDeclSyntax ) -> SyntaxVisitorContinueKind {
4155+ guard let inheritanceClause = node. inheritanceClause else { return . visitChildren }
4156+ let target = node. extendedType. trimmedDescription
4157+ for inherited in inheritanceClause. inheritedTypes {
4158+ if let name = parent. resolveJSProtocolConstraint ( for: inherited. type) {
4159+ protocolsByTarget [ target, default: [ ] ] . append ( name)
4160+ }
4161+ }
4162+ return . visitChildren
4163+ }
4164+ }
0 commit comments