Skip to content

Commit 8a9ecd5

Browse files
committed
Fixes a bug where the errors variable could be mutated on multiple threads by switching to Structured Concurrency from GCD, allowing the compiler to prevent future bugs of this nature from being written
1 parent fee7838 commit 8a9ecd5

File tree

1 file changed

+50
-45
lines changed

1 file changed

+50
-45
lines changed

CodeGeneration/Sources/generate-swift-syntax/GenerateSwiftSyntax.swift

Lines changed: 50 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ struct TemplateSpec {
6868
}
6969

7070
@main
71-
struct GenerateSwiftSyntax: ParsableCommand {
71+
struct GenerateSwiftSyntax: AsyncParsableCommand {
7272
@Argument(help: "The path to the source directory (i.e. 'swift-syntax/Sources') where the source files are to be generated")
7373
var destination: String = {
7474
let sourcesURL = URL(fileURLWithPath: #filePath)
@@ -83,7 +83,7 @@ struct GenerateSwiftSyntax: ParsableCommand {
8383
@Flag(help: "Enable verbose output")
8484
var verbose: Bool = false
8585

86-
func run() throws {
86+
func run() async throws {
8787
let destination = URL(fileURLWithPath: self.destination).standardizedFileURL
8888

8989
var fileSpecs: [GeneratedFileSpec] = [
@@ -139,7 +139,6 @@ struct GenerateSwiftSyntax: ParsableCommand {
139139

140140
let modules = Set(fileSpecs.compactMap { $0.pathComponents.first })
141141

142-
let previouslyGeneratedFilesLock = NSLock()
143142
var previouslyGeneratedFiles = Set(
144143
modules.flatMap { (module) -> [URL] in
145144
let generatedDir =
@@ -154,32 +153,37 @@ struct GenerateSwiftSyntax: ParsableCommand {
154153
}
155154
)
156155

157-
var errors: [Error] = []
158-
DispatchQueue.concurrentPerform(iterations: fileSpecs.count) { index in
159-
let fileSpec = fileSpecs[index]
160-
do {
161-
var destination = destination
162-
for component in fileSpec.pathComponents {
163-
destination = destination.appendingPathComponent(component)
156+
await withTaskGroup(of: (url: URL, error: Error?).self) { group in
157+
for fileSpec in fileSpecs {
158+
group.addTask {
159+
do {
160+
var destination = destination
161+
for component in fileSpec.pathComponents {
162+
destination = destination.appendingPathComponent(component)
163+
}
164+
do {
165+
try generateFile(
166+
contents: fileSpec.contents,
167+
destination: destination,
168+
verbose: verbose
169+
)
170+
} catch {
171+
// If we throw from here, we'll lose the URL,
172+
// and we'll end up removing a file that is still
173+
// included in the files we intend to generate,
174+
// even if we failed to do so on this run.
175+
return (destination, error)
176+
}
177+
return (destination, nil)
178+
}
179+
}
180+
}
181+
for await result in group {
182+
_ = previouslyGeneratedFiles.remove(result.url)
183+
if let error = result.error {
184+
print("Error when generating file at \(result.url):", error)
164185
}
165-
166-
previouslyGeneratedFilesLock.lock();
167-
_ = previouslyGeneratedFiles.remove(destination)
168-
previouslyGeneratedFilesLock.unlock()
169-
170-
try generateFile(
171-
contents: fileSpec.contents,
172-
destination: destination,
173-
verbose: verbose
174-
)
175-
} catch {
176-
errors.append(error)
177186
}
178-
}
179-
180-
if let firstError = errors.first {
181-
// TODO: It would be nice if we could emit all errors
182-
throw firstError
183187
}
184188

185189
for file in previouslyGeneratedFiles {
@@ -189,24 +193,25 @@ struct GenerateSwiftSyntax: ParsableCommand {
189193
}
190194
}
191195

192-
private func generateFile(
193-
contents: @autoclosure () -> String,
194-
destination: URL,
195-
verbose: Bool
196-
) throws {
197-
try FileManager.default.createDirectory(
198-
atPath: destination.deletingLastPathComponent().path,
199-
withIntermediateDirectories: true,
200-
attributes: nil
201-
)
196+
}
202197

203-
if verbose {
204-
print("Generating \(destination.path)...")
205-
}
206-
let start = Date()
207-
try contents().write(to: destination, atomically: true, encoding: .utf8)
208-
if verbose {
209-
print("Generated \(destination.path) in \((Date().timeIntervalSince(start) * 1000).rounded() / 1000)s")
210-
}
198+
fileprivate func generateFile(
199+
contents: @autoclosure () -> String,
200+
destination: URL,
201+
verbose: Bool
202+
) throws {
203+
try FileManager.default.createDirectory(
204+
atPath: destination.deletingLastPathComponent().path,
205+
withIntermediateDirectories: true,
206+
attributes: nil
207+
)
208+
209+
if verbose {
210+
print("Generating \(destination.path)...")
211+
}
212+
let start = Date()
213+
try contents().write(to: destination, atomically: true, encoding: .utf8)
214+
if verbose {
215+
print("Generated \(destination.path) in \((Date().timeIntervalSince(start) * 1000).rounded() / 1000)s")
211216
}
212217
}

0 commit comments

Comments
 (0)