diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index 6952f3b1e..e07338101 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -201,18 +201,22 @@ public func collectAllFiles(suffix: String, in inputPaths: [URL], log: Logger) - ] for path in inputPaths { + var filesInPath: OrderedSet = [] do { try collectFilesFromPath( path, suffix: suffix, fileManager: fileManager, resourceKeys: resourceKeys, - into: &allFiles, + into: &filesInPath, log: log ) } catch { log.trace("Failed to collect paths in: \(path), skipping.") } + // Directory enumeration order is filesystem-dependent (e.g. hash-ordered on APFS); + // sort so that extraction output is deterministic across machines. + allFiles.append(contentsOf: filesInPath.sorted { $0.path < $1.path }) } return allFiles diff --git a/Sources/SwiftExtract/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift b/Sources/SwiftExtract/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift index 08bc4e146..773b7b00f 100644 --- a/Sources/SwiftExtract/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift +++ b/Sources/SwiftExtract/SwiftTypes/SwiftParsedModuleSymbolTableBuilder.swift @@ -27,8 +27,16 @@ package struct SwiftParsedModuleSymbolTableBuilder { /// The build configuration used to resolve #if conditional compilation blocks. package let buildConfig: any BuildConfiguration - /// Extension decls their extended type hasn't been resolved. - package var unresolvedExtensions: [ExtensionDeclSyntax] + /// An extension decl whose extended type hasn't been resolved yet, paired with + /// the path of the source file it was declared in. + package struct UnresolvedExtension { + package var node: ExtensionDeclSyntax + package var sourceFilePath: String + } + + /// Extension decls their extended type hasn't been resolved, paired with + /// the path of the source file they were declared in. + package var unresolvedExtensions: [UnresolvedExtension] package init( moduleName: String, @@ -213,7 +221,7 @@ extension SwiftParsedModuleSymbolTableBuilder { sourceFilePath: String ) { if !self.tryHandle(extension: node, sourceFilePath: sourceFilePath) { - self.unresolvedExtensions.append(node) + self.unresolvedExtensions.append(UnresolvedExtension(node: node, sourceFilePath: sourceFilePath)) } } @@ -273,7 +281,7 @@ extension SwiftParsedModuleSymbolTableBuilder { while !unresolvedExtensions.isEmpty { var extensions = self.unresolvedExtensions extensions.removeAll(where: { - self.tryHandle(extension: $0, sourceFilePath: "FIXME_MISSING_FILEPATH.swift") // FIXME: missing filepath here in finalize + self.tryHandle(extension: $0.node, sourceFilePath: $0.sourceFilePath) }) // If we didn't resolve anything, we're done. diff --git a/Tests/SwiftExtractTests/SwiftSymbolTableTests.swift b/Tests/SwiftExtractTests/SwiftSymbolTableTests.swift index 4b8e44dbd..8648a95df 100644 --- a/Tests/SwiftExtractTests/SwiftSymbolTableTests.swift +++ b/Tests/SwiftExtractTests/SwiftSymbolTableTests.swift @@ -54,6 +54,37 @@ struct SwiftSymbolTableSuite { #expect(symbolTable.lookupType("Z", parent: nil) == nil) } + @Test func deferredExtensionKeepsSourceFilePath() throws { + // The extension is visited before the file declaring the extended type, + // so it is resolved late (in `finalize()`); nested types must still be + // attributed to the file they were declared in. + let extensionFile: SourceFileSyntax = """ + extension Outer.Inner { + public struct Deferred {} + } + """ + let declarationFile: SourceFileSyntax = """ + public enum Outer { + public enum Inner {} + } + """ + let symbolTable = SwiftSymbolTable.setup( + moduleName: "MyModule", + [ + .init(syntax: extensionFile, path: "Feature/Feature+Model.swift"), + .init(syntax: declarationFile, path: "Core/Outer.swift"), + ], + config: nil, + sourceDependencies: SourceDependencies(), + ) + + let outer = try #require(symbolTable.lookupType("Outer", parent: nil)) + let inner = try #require(symbolTable.lookupType("Inner", parent: outer)) + let deferred = try #require(symbolTable.lookupType("Deferred", parent: inner)) + #expect(deferred.sourceFilePath == "Feature/Feature+Model.swift") + #expect(inner.sourceFilePath == "Core/Outer.swift") + } + @Test func moduleScopedLookup() throws { let symbolTable = makeSymbolTable( moduleName: "MyModule",