@@ -178,6 +178,15 @@ CancellationToken cancellationToken
178178 writer = new FileSystemFormattedFileWriter ( fileSystem ) ;
179179 }
180180
181+ // Collect individual file paths so they can be processed in batches
182+ // instead of one-by-one (which re-creates OptionsProvider, reads/writes
183+ // the formatting cache, and loses parallelism for every single file).
184+ var pendingFiles = new List < (
185+ string DirectoryName ,
186+ string ActualPath ,
187+ string OriginalPath
188+ ) > ( ) ;
189+
181190 for ( var x = 0 ; x < commandLineOptions . DirectoryOrFilePaths . Length ; x ++ )
182191 {
183192 var directoryOrFilePath = commandLineOptions . DirectoryOrFilePaths [ x ] ;
@@ -193,30 +202,8 @@ CancellationToken cancellationToken
193202 return 1 ;
194203 }
195204
196- var directoryName = isFile
197- ? fileSystem . Path . GetDirectoryName ( directoryOrFilePath )
198- : directoryOrFilePath ;
199-
200- ArgumentNullException . ThrowIfNull ( directoryName ) ;
201-
202- var optionsProvider = await OptionsProvider . Create (
203- directoryName ,
204- commandLineOptions . ConfigPath ,
205- commandLineOptions . IgnorePath ,
206- fileSystem ,
207- logger ,
208- cancellationToken
209- ) ;
210-
211205 var originalDirectoryOrFile = commandLineOptions . OriginalDirectoryOrFilePaths [ x ] ;
212206
213- var formattingCache = await FormattingCacheFactory . InitializeAsync (
214- commandLineOptions ,
215- optionsProvider ,
216- fileSystem ,
217- cancellationToken
218- ) ;
219-
220207 if ( ! Path . IsPathRooted ( originalDirectoryOrFile ) )
221208 {
222209 if ( ! originalDirectoryOrFile . StartsWith ( '.' ) )
@@ -226,81 +213,32 @@ CancellationToken cancellationToken
226213 }
227214 }
228215
229- async IAsyncEnumerable < string > EnumerateNonignoredFiles ( string directory )
216+ if ( isFile )
230217 {
231- foreach ( var file in fileSystem . Directory . EnumerateFiles ( directory ) )
232- {
233- yield return file ;
234- }
235-
236- foreach ( var subdirectory in fileSystem . Directory . EnumerateDirectories ( directory ) )
237- {
238- if ( await optionsProvider . IsIgnoredAsync ( subdirectory , cancellationToken ) )
239- {
240- continue ;
241- }
242-
243- await foreach ( var file in EnumerateNonignoredFiles ( subdirectory ) )
244- {
245- yield return file ;
246- }
247- }
218+ var directoryName = fileSystem . Path . GetDirectoryName ( directoryOrFilePath ) ;
219+ ArgumentNullException . ThrowIfNull ( directoryName ) ;
220+ pendingFiles . Add ( ( directoryName , directoryOrFilePath , originalDirectoryOrFile ) ) ;
248221 }
249-
250- async Task FormatFile (
251- string actualFilePath ,
252- string originalFilePath ,
253- bool warnForUnsupported = false
254- )
222+ else if ( isDirectory )
255223 {
256- if (
257- (
258- ! commandLineOptions . IncludeGenerated
259- && GeneratedCodeUtilities . IsGeneratedCodeFile ( actualFilePath )
260- ) || await optionsProvider . IsIgnoredAsync ( actualFilePath , cancellationToken )
261- )
262- {
263- return ;
264- }
224+ var directoryName = directoryOrFilePath ;
265225
266- var printerOptions = await optionsProvider . GetPrinterOptionsForAsync (
267- actualFilePath ,
226+ var optionsProvider = await OptionsProvider . Create (
227+ directoryName ,
228+ commandLineOptions . ConfigPath ,
229+ commandLineOptions . IgnorePath ,
230+ fileSystem ,
231+ logger ,
268232 cancellationToken
269233 ) ;
270234
271- if ( printerOptions is { Formatter : not Formatter . Unknown } )
272- {
273- printerOptions . IncludeGenerated = commandLineOptions . IncludeGenerated ;
274- await FormatPhysicalFile (
275- actualFilePath ,
276- originalFilePath ,
277- fileSystem ,
278- logger ,
279- commandLineFormatterResult ,
280- writer ,
281- commandLineOptions ,
282- printerOptions ,
283- formattingCache ,
284- cancellationToken
285- ) ;
286- }
287- else if ( warnForUnsupported )
288- {
289- var fileIssueLogger = new FileIssueLogger (
290- originalFilePath ,
291- logger ,
292- logFormat : LogFormat . Console
293- ) ;
294- fileIssueLogger . WriteWarning ( "Is an unsupported file type." ) ;
295- }
296- }
235+ var formattingCache = await FormattingCacheFactory . InitializeAsync (
236+ commandLineOptions ,
237+ optionsProvider ,
238+ fileSystem ,
239+ cancellationToken
240+ ) ;
297241
298- if ( isFile )
299- {
300- await FormatFile ( directoryOrFilePath , originalDirectoryOrFile , true ) ;
301- }
302- else if ( isDirectory )
303- {
304242 if (
305243 ! commandLineOptions . NoMSBuildCheck
306244 && await HasMismatchedCliAndMsBuildVersions . Check (
@@ -314,14 +252,47 @@ await FormatPhysicalFile(
314252 return 1 ;
315253 }
316254
255+ async IAsyncEnumerable < string > EnumerateNonignoredFiles ( string directory )
256+ {
257+ foreach ( var file in fileSystem . Directory . EnumerateFiles ( directory ) )
258+ {
259+ yield return file ;
260+ }
261+
262+ foreach (
263+ var subdirectory in fileSystem . Directory . EnumerateDirectories ( directory )
264+ )
265+ {
266+ if (
267+ await optionsProvider . IsIgnoredAsync ( subdirectory , cancellationToken )
268+ )
269+ {
270+ continue ;
271+ }
272+
273+ await foreach ( var file in EnumerateNonignoredFiles ( subdirectory ) )
274+ {
275+ yield return file ;
276+ }
277+ }
278+ }
279+
317280 var tasks = new List < Task > ( ) ;
318281 await foreach (
319282 var file in EnumerateNonignoredFiles ( directoryOrFilePath )
320283 . WithCancellation ( cancellationToken )
321284 )
322285 {
323286 var relativePath = file . Replace ( directoryOrFilePath , originalDirectoryOrFile ) ;
324- tasks . Add ( FormatFile ( file , relativePath ) ) ;
287+ tasks . Add (
288+ FormatFile (
289+ file ,
290+ relativePath ,
291+ optionsProvider ,
292+ formattingCache ,
293+ warnForUnsupported : false
294+ )
295+ ) ;
325296 }
326297
327298 try
@@ -335,12 +306,123 @@ var file in EnumerateNonignoredFiles(directoryOrFilePath)
335306 throw ;
336307 }
337308 }
309+
310+ await formattingCache . ResolveAsync ( cancellationToken ) ;
311+ }
312+ }
313+
314+ // Process individual files using a single shared OptionsProvider
315+ // rooted at their common ancestor directory. This mirrors the directory
316+ // path: one config lookup with internal caching, one formatting cache,
317+ // and parallel formatting.
318+ if ( pendingFiles . Count > 0 )
319+ {
320+ // Find the common ancestor directory of all pending files so we
321+ // create exactly one OptionsProvider whose internal caches cover
322+ // every subdirectory (same as the directory-input path).
323+ var commonRoot = pendingFiles [ 0 ] . DirectoryName ;
324+ for ( var i = 1 ; i < pendingFiles . Count ; i ++ )
325+ {
326+ commonRoot = GetCommonAncestor ( commonRoot , pendingFiles [ i ] . DirectoryName ) ;
327+ }
328+
329+ var optionsProvider = await OptionsProvider . Create (
330+ commonRoot ,
331+ commandLineOptions . ConfigPath ,
332+ commandLineOptions . IgnorePath ,
333+ fileSystem ,
334+ logger ,
335+ cancellationToken
336+ ) ;
337+
338+ var sharedFormattingCache = await FormattingCacheFactory . InitializeAsync (
339+ commandLineOptions ,
340+ optionsProvider ,
341+ fileSystem ,
342+ cancellationToken
343+ ) ;
344+
345+ var tasks = new List < Task > ( ) ;
346+
347+ foreach ( var ( _, actualPath , originalPath ) in pendingFiles )
348+ {
349+ tasks . Add (
350+ FormatFile (
351+ actualPath ,
352+ originalPath ,
353+ optionsProvider ,
354+ sharedFormattingCache ,
355+ warnForUnsupported : true
356+ )
357+ ) ;
358+ }
359+
360+ try
361+ {
362+ await Task . WhenAll ( tasks ) . WaitAsync ( cancellationToken ) ;
363+ }
364+ catch ( OperationCanceledException ex )
365+ {
366+ if ( ex . CancellationToken != cancellationToken )
367+ {
368+ throw ;
369+ }
338370 }
339371
340- await formattingCache . ResolveAsync ( cancellationToken ) ;
372+ await sharedFormattingCache . ResolveAsync ( cancellationToken ) ;
341373 }
342374
343375 return 0 ;
376+
377+ async Task FormatFile (
378+ string actualFilePath ,
379+ string originalFilePath ,
380+ OptionsProvider optionsProvider ,
381+ IFormattingCache formattingCache ,
382+ bool warnForUnsupported
383+ )
384+ {
385+ if (
386+ (
387+ ! commandLineOptions . IncludeGenerated
388+ && GeneratedCodeUtilities . IsGeneratedCodeFile ( actualFilePath )
389+ ) || await optionsProvider . IsIgnoredAsync ( actualFilePath , cancellationToken )
390+ )
391+ {
392+ return ;
393+ }
394+
395+ var printerOptions = await optionsProvider . GetPrinterOptionsForAsync (
396+ actualFilePath ,
397+ cancellationToken
398+ ) ;
399+
400+ if ( printerOptions is { Formatter : not Formatter . Unknown } )
401+ {
402+ printerOptions . IncludeGenerated = commandLineOptions . IncludeGenerated ;
403+ await FormatPhysicalFile (
404+ actualFilePath ,
405+ originalFilePath ,
406+ fileSystem ,
407+ logger ,
408+ commandLineFormatterResult ,
409+ writer ,
410+ commandLineOptions ,
411+ printerOptions ,
412+ formattingCache ,
413+ cancellationToken
414+ ) ;
415+ }
416+ else if ( warnForUnsupported )
417+ {
418+ var fileIssueLogger = new FileIssueLogger (
419+ originalFilePath ,
420+ logger ,
421+ logFormat : LogFormat . Console
422+ ) ;
423+ fileIssueLogger . WriteWarning ( "Is an unsupported file type." ) ;
424+ }
425+ }
344426 }
345427
346428 private static async Task FormatPhysicalFile (
@@ -386,6 +468,29 @@ await PerformFormattingSteps(
386468 ) ;
387469 }
388470
471+ internal static string GetCommonAncestor ( string pathA , string pathB )
472+ {
473+ var separators = new [ ] { Path . DirectorySeparatorChar , Path . AltDirectorySeparatorChar } ;
474+ var partsA = pathA . Split ( separators ) ;
475+ var partsB = pathB . Split ( separators ) ;
476+ var commonLength = 0 ;
477+ for ( var i = 0 ; i < Math . Min ( partsA . Length , partsB . Length ) ; i ++ )
478+ {
479+ if ( string . Equals ( partsA [ i ] , partsB [ i ] , StringComparison . Ordinal ) )
480+ {
481+ commonLength = i + 1 ;
482+ }
483+ else
484+ {
485+ break ;
486+ }
487+ }
488+
489+ return commonLength > 0
490+ ? string . Join ( Path . DirectorySeparatorChar , partsA . Take ( commonLength ) )
491+ : pathA ;
492+ }
493+
389494 private static int ReturnExitCode (
390495 CommandLineOptions commandLineOptions ,
391496 CommandLineFormatterResult result
0 commit comments