Skip to content

Commit 2e474ed

Browse files
dfedclaude
andcommitted
Fix output filename collisions and non-deterministic multi-root ordering
Disambiguate output filenames when multiple root files share the same base name (e.g. ModuleA/Root.swift and ModuleB/Root.swift now produce ModuleA_Root+SafeDI.swift and ModuleB_Root+SafeDI.swift). Sort extensions before joining when multiple roots share a source file, ensuring deterministic output regardless of task-group completion order. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 61c8128 commit 2e474ed

4 files changed

Lines changed: 30 additions & 12 deletions

File tree

Plugins/SafeDIGenerator/SafeDIGenerateDependencyTree.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {
5353
return []
5454
}
5555

56-
let outputFiles = rootFiles.map {
57-
outputDirectory.appending(path: outputFileName(for: $0))
56+
let outputFiles = zip(rootFiles, outputFileNames(for: rootFiles)).map { _, name in
57+
outputDirectory.appending(path: name)
5858
}
5959

6060
let packageRoot = context.package.directoryURL

Plugins/SafeDIPrebuiltGenerator/SafeDIGenerateDependencyTree.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {
5353
return []
5454
}
5555

56-
let outputFiles = rootFiles.map {
57-
outputDirectory.appending(path: outputFileName(for: $0))
56+
let outputFiles = zip(rootFiles, outputFileNames(for: rootFiles)).map { _, name in
57+
outputDirectory.appending(path: name)
5858
}
5959

6060
let packageRoot = context.package.directoryURL

Plugins/Shared.swift

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,27 @@ func findFilesWithRoots(in swiftFiles: [URL]) -> [URL] {
133133
}
134134
}
135135

136-
/// Derive the output filename for a dependency tree generated from an input Swift file.
137-
func outputFileName(for inputURL: URL) -> String {
138-
let baseName = inputURL.deletingPathExtension().lastPathComponent
139-
return "\(baseName)+SafeDI.swift"
136+
/// Derive unique output filenames for a set of input Swift files.
137+
/// If two files share the same base name (e.g. `ModuleA/Root.swift` and `ModuleB/Root.swift`),
138+
/// parent directory components are prepended to disambiguate (e.g. `ModuleA_Root+SafeDI.swift`).
139+
func outputFileNames(for inputURLs: [URL]) -> [String] {
140+
let baseNames = inputURLs.map { $0.deletingPathExtension().lastPathComponent }
141+
142+
// Count occurrences of each base name.
143+
var nameCounts = [String: Int]()
144+
for name in baseNames {
145+
nameCounts[name, default: 0] += 1
146+
}
147+
148+
return zip(inputURLs, baseNames).map { url, baseName in
149+
if nameCounts[baseName, default: 1] > 1 {
150+
// Disambiguate by prepending the parent directory name.
151+
let parent = url.deletingLastPathComponent().lastPathComponent
152+
return "\(parent)_\(baseName)+SafeDI.swift"
153+
} else {
154+
return "\(baseName)+SafeDI.swift"
155+
}
156+
}
140157
}
141158

142159
/// Compute a path string relative to a base directory, for use in the CSV and manifest.
@@ -165,11 +182,12 @@ func writeManifest(
165182
to manifestURL: URL,
166183
relativeTo base: URL,
167184
) throws {
185+
let fileNames = outputFileNames(for: dependencyTreeInputFiles)
168186
var entries = [[String: String]]()
169-
for inputURL in dependencyTreeInputFiles {
187+
for (inputURL, fileName) in zip(dependencyTreeInputFiles, fileNames) {
170188
entries.append([
171189
"inputFilePath": relativePath(for: inputURL, relativeTo: base),
172-
"outputFilePath": outputDirectory.appending(path: outputFileName(for: inputURL)).path(percentEncoded: false),
190+
"outputFilePath": outputDirectory.appending(path: fileName).path(percentEncoded: false),
173191
])
174192
}
175193
let manifest = ["dependencyTreeGeneration": entries]

Sources/SafeDITool/SafeDITool.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import SafeDICore
2424
import SwiftParser
2525

2626
@main
27-
struct SafeDITool: AsyncParsableCommand, Sendable {
27+
struct SafeDITool: AsyncParsableCommand {
2828
// MARK: Arguments
2929

3030
@Argument(help: "A path to a CSV file containing paths of Swift files to parse.") var swiftSourcesFilePath: String?
@@ -198,7 +198,7 @@ struct SafeDITool: AsyncParsableCommand, Sendable {
198198
throw ManifestError.noRootFound(inputPath: entry.inputFilePath)
199199
}
200200
let code: String = if let extensions = sourceFileToExtensions[entry.inputFilePath] {
201-
fileHeader + extensions.joined(separator: "\n\n")
201+
fileHeader + extensions.sorted().joined(separator: "\n\n")
202202
} else {
203203
emptyRootContent
204204
}

0 commit comments

Comments
 (0)