@@ -6,7 +6,7 @@ import Foundation
66/// A command line tool that formats the given directories using SwiftFormat and SwiftLint,
77/// based on the Airbnb Swift Style Guide
88@main
9- struct AirbnbSwiftFormatTool : ParsableCommand {
9+ struct AirbnbSwiftFormatTool : AsyncParsableCommand {
1010
1111 // MARK: Internal
1212
@@ -40,64 +40,134 @@ struct AirbnbSwiftFormatTool: ParsableCommand {
4040 @Option ( help: " The project's minimum Swift version " )
4141 var swiftVersion : String ?
4242
43- func run( ) throws {
44- let swiftFormat = makeSwiftFormatCommand ( )
45- let swiftLint = makeSwiftLintCommand ( autocorrect: false )
46- let swiftLintAutocorrect = makeSwiftLintCommand ( autocorrect: true )
43+ func run( ) async throws {
44+ // Process all directories in parallel, each running the full pipeline independently.
45+ // We use withTaskGroup (not withThrowingTaskGroup) to ensure ALL directories are processed
46+ // even if some fail - this gives users complete feedback about all issues.
47+ let results = await withTaskGroup ( of: Result< DirectoryResult, Error> . self ) { group in
48+ for directory in directories {
49+ group. addTask {
50+ do {
51+ return . success( try await processDirectory ( directory) )
52+ } catch {
53+ return . failure( DirectoryError ( directory: directory, underlyingError: error) )
54+ }
55+ }
56+ }
57+
58+ var results = [ Result < DirectoryResult , Error > ] ( )
59+ for await result in group {
60+ results. append ( result)
61+ }
62+ return results
63+ }
64+
65+ try aggregateResults ( results)
66+ }
4767
48- let swiftFormatExitCode = try swiftFormat. run ( )
68+ // MARK: Private
69+
70+ /// Whether the command should autocorrect invalid code, or only emit lint errors
71+ private var lintOnly : Bool {
72+ lint
73+ }
74+
75+ /// Processes a single directory through the full formatting pipeline
76+ /// - SwiftFormat -> SwiftLint autocorrect (if not lint-only) -> SwiftLint lint
77+ private func processDirectory( _ directory: String ) async throws -> DirectoryResult {
78+ let swiftFormatExitCode = try await makeSwiftFormatCommand ( for: directory) . runAsync ( )
4979
5080 // Run SwiftLint in autocorrect mode first, so that if autocorrect fixes all of the SwiftLint violations
5181 // then the following lint-only invocation will not report any violations.
5282 let swiftLintAutocorrectExitCode : Int32 ?
53- if
54- // When only linting, we shouldn't run SwiftLint with autocorrect enabled
55- !lintOnly
56- {
57- swiftLintAutocorrectExitCode = try swiftLintAutocorrect. run ( )
83+ if !lintOnly {
84+ swiftLintAutocorrectExitCode = try await makeSwiftLintCommand ( for: directory, autocorrect: true ) . runAsync ( )
5885 } else {
5986 swiftLintAutocorrectExitCode = nil
6087 }
6188
6289 // We always have to run SwiftLint in lint-only mode at least once,
6390 // because when in autocorrect mode SwiftLint won't emit any lint warnings.
64- let swiftLintExitCode = try swiftLint . run ( )
91+ let swiftLintExitCode = try await makeSwiftLintCommand ( for : directory , autocorrect : false ) . runAsync ( )
6592
66- if
67- swiftFormatExitCode == SwiftFormatExitCode . lintFailure ||
68- swiftLintExitCode == SwiftLintExitCode . lintFailure ||
69- swiftLintAutocorrectExitCode == SwiftLintExitCode . lintFailure
70- {
71- throw ExitCode . failure
72- }
93+ return DirectoryResult (
94+ directory : directory ,
95+ swiftFormatExitCode : swiftFormatExitCode ,
96+ swiftLintExitCode : swiftLintExitCode ,
97+ swiftLintAutocorrectExitCode : swiftLintAutocorrectExitCode
98+ )
99+ }
73100
74- // Any other non-success exit code is an unknown failure
75- if swiftFormatExitCode != EXIT_SUCCESS {
76- throw ExitCode ( swiftFormatExitCode)
101+ /// Aggregates results from all directories and throws appropriate errors
102+ private func aggregateResults( _ results: [ Result < DirectoryResult , Error > ] ) throws {
103+ var successResults = [ DirectoryResult] ( )
104+ var executionErrors = [ DirectoryError] ( )
105+
106+ // Separate successes from execution errors
107+ for result in results {
108+ switch result {
109+ case . success( let directoryResult) :
110+ successResults. append ( directoryResult)
111+ case . failure( let error) :
112+ if let directoryError = error as? DirectoryError {
113+ executionErrors. append ( directoryError)
114+ } else {
115+ // Defensive: wrap unexpected error types to avoid silent failures
116+ executionErrors. append ( DirectoryError ( directory: " unknown " , underlyingError: error) )
117+ }
118+ }
77119 }
78120
79- if swiftLintExitCode != EXIT_SUCCESS {
80- throw ExitCode ( swiftLintExitCode)
121+ // Report any execution errors (e.g., binary not found)
122+ if let firstError = executionErrors. first {
123+ for error in executionErrors {
124+ log ( " Failed to process ' \( error. directory) ': \( error. underlyingError. localizedDescription) " )
125+ }
126+ throw firstError. underlyingError
81127 }
82128
83- if
84- let swiftLintAutocorrectExitCode = swiftLintAutocorrectExitCode,
85- swiftLintAutocorrectExitCode != EXIT_SUCCESS
86- {
87- throw ExitCode ( swiftLintAutocorrectExitCode)
129+ // Check for lint failures and report which directories had issues
130+ var directoriesWithLintFailures = [ String] ( )
131+ for result in successResults {
132+ if
133+ result. swiftFormatExitCode == SwiftFormatExitCode . lintFailure ||
134+ result. swiftLintExitCode == SwiftLintExitCode . lintFailure ||
135+ result. swiftLintAutocorrectExitCode == SwiftLintExitCode . lintFailure
136+ {
137+ directoriesWithLintFailures. append ( result. directory)
138+ }
88139 }
89- }
90140
91- // MARK: Private
141+ if !directoriesWithLintFailures. isEmpty {
142+ log ( " Lint failures in: \( directoriesWithLintFailures. joined ( separator: " , " ) ) " )
143+ throw ExitCode . failure
144+ }
92145
93- /// Whether the command should autocorrect invalid code, or only emit lint errors
94- private var lintOnly : Bool {
95- lint
146+ // Any other non-success exit code is an unknown failure
147+ for result in successResults {
148+ if result. swiftFormatExitCode != EXIT_SUCCESS {
149+ log ( " SwiftFormat failed in ' \( result. directory) ' with exit code \( result. swiftFormatExitCode) " )
150+ throw ExitCode ( result. swiftFormatExitCode)
151+ }
152+
153+ if result. swiftLintExitCode != EXIT_SUCCESS {
154+ log ( " SwiftLint failed in ' \( result. directory) ' with exit code \( result. swiftLintExitCode) " )
155+ throw ExitCode ( result. swiftLintExitCode)
156+ }
157+
158+ if
159+ let swiftLintAutocorrectExitCode = result. swiftLintAutocorrectExitCode,
160+ swiftLintAutocorrectExitCode != EXIT_SUCCESS
161+ {
162+ log ( " SwiftLint autocorrect failed in ' \( result. directory) ' with exit code \( swiftLintAutocorrectExitCode) " )
163+ throw ExitCode ( swiftLintAutocorrectExitCode)
164+ }
165+ }
96166 }
97167
98- /// Builds a command that runs the SwiftFormat tool
99- private func makeSwiftFormatCommand( ) -> Command {
100- var arguments = directories + [
168+ /// Builds a command that runs the SwiftFormat tool on a single directory
169+ private func makeSwiftFormatCommand( for directory : String ) -> Command {
170+ var arguments = [ directory ] + [
101171 " --config " ,
102172 swiftFormatConfig,
103173 ]
@@ -121,11 +191,11 @@ struct AirbnbSwiftFormatTool: ParsableCommand {
121191 )
122192 }
123193
124- /// Builds a command that runs the SwiftLint tool
194+ /// Builds a command that runs the SwiftLint tool on a single directory
125195 /// - If `autocorrect` is true, passes the `--fix` flag to SwiftLint.
126196 /// When autocorrecting, SwiftLint doesn't emit any lint warnings.
127- private func makeSwiftLintCommand( autocorrect: Bool ) -> Command {
128- var arguments = directories + [
197+ private func makeSwiftLintCommand( for directory : String , autocorrect: Bool ) -> Command {
198+ var arguments = [ directory ] + [
129199 " --config " ,
130200 swiftLintConfig,
131201 // Required for SwiftLint to emit a non-zero exit code on lint failure
@@ -175,3 +245,21 @@ enum SwiftFormatExitCode {
175245enum SwiftLintExitCode {
176246 static let lintFailure : Int32 = 2
177247}
248+
249+ // MARK: - DirectoryResult
250+
251+ /// The result of running the formatting pipeline on a single directory
252+ struct DirectoryResult : Sendable {
253+ let directory : String
254+ let swiftFormatExitCode : Int32
255+ let swiftLintExitCode : Int32
256+ let swiftLintAutocorrectExitCode : Int32 ?
257+ }
258+
259+ // MARK: - DirectoryError
260+
261+ /// An error that occurred while processing a specific directory
262+ struct DirectoryError : Error {
263+ let directory : String
264+ let underlyingError : Error
265+ }
0 commit comments