@@ -139,6 +139,39 @@ public final class SwiftToSkeleton {
139139 sourceFiles . append ( ( sourceFile , inputFilePath ) )
140140 }
141141
142+ private func resolveDeferredExtensions ( _ exportCollectors : [ ExportSwiftAPICollector ] ) {
143+ var pendingExtensions = exportCollectors . flatMap { collector in
144+ collector . deferredExtensions . map { ( owner : collector , declaration : $0 ) }
145+ }
146+ var previousCount : Int
147+ // An extended type might be defined in another extension, so keep resolving until no more progress is made.
148+ repeat {
149+ previousCount = pendingExtensions . count
150+ var nextPendingExtensions : [ ( owner : ExportSwiftAPICollector , declaration : ExtensionDeclSyntax ) ] = [ ]
151+ for pending in pendingExtensions {
152+ if ! resolveExtension ( pending . declaration , in : exportCollectors ) {
153+ nextPendingExtensions . append ( pending )
154+ }
155+ }
156+ pendingExtensions = nextPendingExtensions
157+ } while pendingExtensions . count < previousCount
158+ for pending in pendingExtensions {
159+ pending . owner . diagnoseUnresolvedExtension ( pending . declaration )
160+ }
161+ }
162+
163+ private func resolveExtension (
164+ _ declaration : ExtensionDeclSyntax ,
165+ in exportCollectors : [ ExportSwiftAPICollector ]
166+ ) -> Bool {
167+ for collector in exportCollectors {
168+ if collector . resolveExtension ( declaration ) {
169+ return true
170+ }
171+ }
172+ return false
173+ }
174+
142175 public func finalize ( ) throws -> BridgeJSSkeleton {
143176 var perSourceErrors : [ ( inputFilePath : String , errors : [ DiagnosticError ] ) ] = [ ]
144177 var importedFiles : [ ImportedFileSkeleton ] = [ ]
@@ -244,9 +277,7 @@ public final class SwiftToSkeleton {
244277 }
245278
246279 // Resolve extensions against all collectors. This needs to happen at this point so we can resolve both same file and cross file extensions.
247- for source in exportCollectors {
248- source . resolveDeferredExtensions ( against : exportCollectors )
249- }
280+ resolveDeferredExtensions ( exportCollectors )
250281
251282 // We have to collect diagnostics after all deferred extensions are resolved, since they could generate some.
252283 for ( ( _, inputFilePath ) , exportCollector ) in zip ( sourceFiles , exportCollectors ) {
@@ -536,12 +567,12 @@ public final class SwiftToSkeleton {
536567
537568 if let typeDecl = typeDeclResolver . resolve ( type ) {
538569 if typeDecl . is ( ProtocolDeclSyntax . self ) {
539- let swiftCallName = SwiftToSkeleton . computeSwiftCallName ( for : typeDecl , itemName : typeDecl . name . text )
570+ let swiftCallName = computeSwiftCallName ( for : typeDecl , itemName : typeDecl . name . text )
540571 return . swiftProtocol ( swiftCallName )
541572 }
542573
543574 if let enumDecl = typeDecl . as ( EnumDeclSyntax . self ) {
544- let swiftCallName = SwiftToSkeleton . computeSwiftCallName ( for : enumDecl , itemName : enumDecl . name . text )
575+ let swiftCallName = computeSwiftCallName ( for : enumDecl , itemName : enumDecl . name . text )
545576 if let jsAttribute = enumDecl . attributes . firstJSAttribute ,
546577 let aliasTarget = extractAliasTarget ( from : jsAttribute )
547578 {
@@ -580,7 +611,7 @@ public final class SwiftToSkeleton {
580611 }
581612
582613 if let structDecl = typeDecl . as ( StructDeclSyntax . self ) {
583- let swiftCallName = SwiftToSkeleton . computeSwiftCallName (
614+ let swiftCallName = computeSwiftCallName (
584615 for : structDecl ,
585616 itemName : structDecl . name . text
586617 )
@@ -598,7 +629,7 @@ public final class SwiftToSkeleton {
598629 guard typeDecl . is ( ClassDeclSyntax . self ) || typeDecl . is ( ActorDeclSyntax . self ) else {
599630 return nil
600631 }
601- let swiftCallName = SwiftToSkeleton . computeSwiftCallName ( for : typeDecl , itemName : typeDecl . name . text )
632+ let swiftCallName = computeSwiftCallName ( for : typeDecl , itemName : typeDecl . name . text )
602633
603634 // A type annotated with @JSClass is a JavaScript object wrapper (imported),
604635 // even if it is declared as a Swift class.
@@ -638,7 +669,7 @@ public final class SwiftToSkeleton {
638669 private func resolveExternal ( for type : TypeSyntax , errors : inout [ DiagnosticError ] ) -> BridgeType ? {
639670 guard
640671 ! externalModuleIndex . isEmpty ,
641- var components = typeDeclResolver . qualifiedComponents ( from : type )
672+ var components = type . qualifiedComponents
642673 else {
643674 return nil
644675 }
@@ -777,27 +808,50 @@ public final class SwiftToSkeleton {
777808 return nil
778809 }
779810
780- /// Computes the full Swift call name by walking up the AST hierarchy to find all parent enums
811+ /// This currently doesn’t work correctly for extensions on types defined in other modules,
812+ /// which is fine for now since we don’t support extending @JS types from other modules.
813+ /// This will need updating when we do.
814+ fileprivate func enclosingDeclarations ( of node : some SyntaxProtocol ) -> [ Syntax ] {
815+ var declarations : [ Syntax ] = [ ]
816+ var visitedExtendedTypes : Set < SyntaxIdentifier > = [ ]
817+ var currentNode : Syntax ? = Syntax ( node ) . parent
818+
819+ while let parent = currentNode {
820+ if let extensionDecl = parent . as ( ExtensionDeclSyntax . self ) {
821+ if let extendedDecl = typeDeclResolver . resolve ( extensionDecl . extendedType ) ,
822+ visitedExtendedTypes . insert ( extendedDecl . id ) . inserted
823+ {
824+ declarations . append ( Syntax ( extendedDecl ) )
825+ currentNode = Syntax ( extendedDecl ) . parent
826+ } else {
827+ currentNode = parent . parent
828+ }
829+ } else {
830+ declarations . append ( parent )
831+ currentNode = parent . parent
832+ }
833+ }
834+ return declarations
835+ }
836+
781837 /// This generates the qualified name needed for Swift code generation (e.g., "Networking.API.HTTPServer")
782- fileprivate static func computeSwiftCallName ( for node : some SyntaxProtocol , itemName : String ) -> String {
838+ fileprivate func computeSwiftCallName ( for node : some SyntaxProtocol , itemName : String ) -> String {
783839 var swiftPath : [ String ] = [ ]
784- var currentNode : Syntax ? = node . parent
785840
786- while let parent = currentNode {
787- if let enumDecl = parent . as ( EnumDeclSyntax . self ) ,
841+ for declaration in enclosingDeclarations ( of : node ) {
842+ if let enumDecl = declaration . as ( EnumDeclSyntax . self ) ,
788843 enumDecl . attributes . hasJSAttribute ( )
789844 {
790845 swiftPath . insert ( enumDecl . name . text , at : 0 )
791- } else if let structDecl = parent . as ( StructDeclSyntax . self ) ,
846+ } else if let structDecl = declaration . as ( StructDeclSyntax . self ) ,
792847 structDecl . attributes . hasJSAttribute ( )
793848 {
794849 swiftPath . insert ( structDecl . name . text , at : 0 )
795- } else if let classDecl = parent . as ( ClassDeclSyntax . self ) ,
850+ } else if let classDecl = declaration . as ( ClassDeclSyntax . self ) ,
796851 classDecl . attributes . hasJSAttribute ( )
797852 {
798853 swiftPath . insert ( classDecl . name . text , at : 0 )
799854 }
800- currentNode = parent . parent
801855 }
802856
803857 if swiftPath . isEmpty {
@@ -1883,7 +1937,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
18831937 resolvedNamespace : namespaceResult . namespace ,
18841938 parentTypeNamespace : computeParentTypeNamespace ( for : node )
18851939 )
1886- let swiftCallName = SwiftToSkeleton . computeSwiftCallName ( for : node , itemName : name )
1940+ let swiftCallName = parent . computeSwiftCallName ( for : node , itemName : name )
18871941 let explicitAccessControl = computeExplicitAtLeastInternalAccessControl (
18881942 for : node ,
18891943 message : " Class visibility must be at least internal "
@@ -1923,23 +1977,15 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
19231977 return . skipChildren
19241978 }
19251979
1926- func resolveDeferredExtensions ( against collectors : [ ExportSwiftAPICollector ] ) {
1927- for ext in deferredExtensions {
1928- var resolved = false
1929- for collector in collectors {
1930- if collector . resolveExtension ( ext ) {
1931- resolved = true
1932- break
1933- }
1934- }
1935- if ! resolved , containsJSAnnotatedDeclaration ( ext . memberBlock . members ) {
1936- diagnose (
1937- node : ext . extendedType ,
1938- message : " Unsupported type ' \( ext . extendedType . trimmedDescription ) '. " ,
1939- hint : " You can only extend `@JS` annotated types defined in the same module "
1940- )
1941- }
1980+ func diagnoseUnresolvedExtension ( _ ext : ExtensionDeclSyntax ) {
1981+ guard containsJSAnnotatedDeclaration ( ext . memberBlock . members ) else {
1982+ return
19421983 }
1984+ diagnose (
1985+ node : ext . extendedType ,
1986+ message : " Unsupported type ' \( ext . extendedType . trimmedDescription ) '. " ,
1987+ hint : " You can only extend `@JS` annotated types defined in the same module "
1988+ )
19431989 }
19441990
19451991 private func containsJSAnnotatedDeclaration ( _ members : MemberBlockItemListSyntax ) -> Bool {
@@ -1949,25 +1995,23 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
19491995 }
19501996
19511997 /// Walks extension members under the matching type’s state, returning whether the type was found.
1952- ///
1953- /// Note: The lookup scans dictionaries keyed by `makeKey(name:namespace:)`, matching only by
1954- /// plain name. If two types share a name but differ by namespace, `.first(where:)` picks
1955- /// whichever comes first. This is acceptable today since namespace collisions are unlikely,
1956- /// but may need refinement if namespace-qualified extension resolution is added.
19571998 func resolveExtension ( _ ext : ExtensionDeclSyntax ) -> Bool {
1958- let name = ext . extendedType . trimmedDescription
1999+ guard let extendedDecl = parent . typeDeclResolver . resolve ( ext . extendedType ) else {
2000+ return false
2001+ }
2002+ let swiftCallName = parent . computeSwiftCallName ( for : extendedDecl , itemName : extendedDecl . name . text )
19592003 let state : State
1960- if let entry = exportedClassByName . first ( where : { $0 . value . name == name } ) {
1961- state = . classBody ( name : name , key : entry . key )
1962- } else if let entry = exportedStructByName . first ( where : { $0 . value . name == name } ) {
1963- state = . structBody ( name : name , key : entry . key )
1964- } else if let entry = exportedEnumByName . first ( where : { $0 . value . name == name } ) {
1965- state = . enumBody ( name : name , key : entry . key )
1966- } else if exportedProtocolByName . values . contains ( where : { $0 . name == name } ) {
2004+ if let entry = exportedClassByName . first ( where : { $0 . value . swiftCallName == swiftCallName } ) {
2005+ state = . classBody ( name : entry . value . name , key : entry . key )
2006+ } else if let entry = exportedStructByName . first ( where : { $0 . value . swiftCallName == swiftCallName } ) {
2007+ state = . structBody ( name : entry . value . name , key : entry . key )
2008+ } else if let entry = exportedEnumByName . first ( where : { $0 . value . swiftCallName == swiftCallName } ) {
2009+ state = . enumBody ( name : entry . value . name , key : entry . key )
2010+ } else if exportedProtocolByName . values . contains ( where : { $0 . name == swiftCallName } ) {
19672011 diagnose (
19682012 node : ext . extendedType ,
19692013 message : " Protocol extensions are not supported by BridgeJS. " ,
1970- hint : " You cannot extend `@JS` protocol ' \( name ) ' with additional members "
2014+ hint : " You cannot extend `@JS` protocol ' \( swiftCallName ) ' with additional members "
19712015 )
19722016 return true
19732017 } else {
@@ -1986,7 +2030,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
19862030 jsAttribute : AttributeSyntax ,
19872031 aliasTarget : TypeSyntax
19882032 ) {
1989- let swiftCallName = SwiftToSkeleton . computeSwiftCallName ( for : node , itemName : node . name . text )
2033+ let swiftCallName = parent . computeSwiftCallName ( for : node , itemName : node . name . text )
19902034 if extractNamespace ( from : jsAttribute ) != nil {
19912035 errors . append (
19922036 DiagnosticError (
@@ -2051,7 +2095,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
20512095 parentTypeNamespace : computeParentTypeNamespace ( for : node )
20522096 )
20532097 let emitStyle = extractEnumStyle ( from : jsAttribute ) ?? . const
2054- let swiftCallName = SwiftToSkeleton . computeSwiftCallName ( for : node , itemName : name )
2098+ let swiftCallName = parent . computeSwiftCallName ( for : node , itemName : name )
20552099 let explicitAccessControl = computeExplicitAtLeastInternalAccessControl (
20562100 for : node ,
20572101 message : " Enum visibility must be at least internal "
@@ -2237,7 +2281,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
22372281 resolvedNamespace : namespaceResult . namespace ,
22382282 parentTypeNamespace : computeParentTypeNamespace ( for : node )
22392283 )
2240- let swiftCallName = SwiftToSkeleton . computeSwiftCallName ( for : node , itemName : name )
2284+ let swiftCallName = parent . computeSwiftCallName ( for : node , itemName : name )
22412285 let explicitAccessControl = computeExplicitAtLeastInternalAccessControl (
22422286 for : node ,
22432287 message : " Struct visibility must be at least internal "
@@ -2552,10 +2596,9 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
25522596 /// Method allows for explicit namespace for top level enum, it will be used as base namespace and will concat enum name
25532597 private func computeNamespace ( for node : some SyntaxProtocol ) -> [ String ] ? {
25542598 var namespace : [ String ] = [ ]
2555- var currentNode : Syntax ? = node . parent
25562599
2557- while let parent = currentNode {
2558- if let enumDecl = parent . as ( EnumDeclSyntax . self ) ,
2600+ for declaration in parent . enclosingDeclarations ( of : node ) {
2601+ if let enumDecl = declaration . as ( EnumDeclSyntax . self ) ,
25592602 enumDecl . attributes . hasJSAttribute ( )
25602603 {
25612604 let isNamespaceEnum = ! enumDecl . memberBlock . members . contains { member in
@@ -2572,27 +2615,24 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
25722615 }
25732616 }
25742617 }
2575- currentNode = parent . parent
25762618 }
25772619
25782620 return namespace . isEmpty ? nil : namespace
25792621 }
25802622
25812623 private func computeParentTypeNamespace ( for node : some SyntaxProtocol ) -> [ String ] ? {
25822624 var path : [ String ] = [ ]
2583- var currentNode : Syntax ? = node . parent
25842625
2585- while let parent = currentNode {
2586- if let structDecl = parent . as ( StructDeclSyntax . self ) ,
2626+ for declaration in parent . enclosingDeclarations ( of : node ) {
2627+ if let structDecl = declaration . as ( StructDeclSyntax . self ) ,
25872628 structDecl . attributes . hasJSAttribute ( )
25882629 {
25892630 path . insert ( structDecl . name . text , at : 0 )
2590- } else if let classDecl = parent . as ( ClassDeclSyntax . self ) ,
2631+ } else if let classDecl = declaration . as ( ClassDeclSyntax . self ) ,
25912632 classDecl . attributes . hasJSAttribute ( )
25922633 {
25932634 path . insert ( classDecl . name . text , at : 0 )
25942635 }
2595- currentNode = parent . parent
25962636 }
25972637
25982638 return path . isEmpty ? nil : path
0 commit comments