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