@@ -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