Skip to content

Commit 7a7a70a

Browse files
authored
Merge pull request #803 from wfltaylor/extension-types
BridgeJS: Allow extensions to contain types
2 parents c3ac9da + 59a6274 commit 7a7a70a

44 files changed

Lines changed: 5672 additions & 101 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 99 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)