diff --git a/lib/src/cli/format_command.dart b/lib/src/cli/format_command.dart index 217a1308..a7ddd4b3 100644 --- a/lib/src/cli/format_command.dart +++ b/lib/src/cli/format_command.dart @@ -154,6 +154,13 @@ final class FormatCommand extends Command { 'See dart.dev/go/experiments.', hide: !verbose, ); + argParser.addMultiOption( + 'exclude', + help: + 'Exclude files and directories matching the given patterns.\n' + 'Patterns can be file paths, directory names, or glob patterns.', + hide: !verbose, + ); if (verbose) argParser.addSeparator('Options when formatting from stdin:'); @@ -312,6 +319,7 @@ final class FormatCommand extends Command { var setExitIfChanged = argResults['set-exit-if-changed'] as bool; var experimentFlags = argResults['enable-experiment'] as List; + var excludePatterns = argResults['exclude'] as List; // If stdin isn't connected to a pipe, then the user is not passing // anything to stdin, so let them know they made a mistake. @@ -339,6 +347,7 @@ final class FormatCommand extends Command { summary: summary, setExitIfChanged: setExitIfChanged, experimentFlags: experimentFlags, + excludePatterns: excludePatterns, ); if (argResults.rest.isEmpty) { diff --git a/lib/src/cli/formatter_options.dart b/lib/src/cli/formatter_options.dart index 487e1336..bdd24b07 100644 --- a/lib/src/cli/formatter_options.dart +++ b/lib/src/cli/formatter_options.dart @@ -54,6 +54,9 @@ final class FormatterOptions { /// See dart.dev/go/experiments for details. final List experimentFlags; + /// Patterns for files and directories to exclude from formatting. + final List excludePatterns; + FormatterOptions({ this.languageVersion, this.indent = 0, @@ -65,7 +68,9 @@ final class FormatterOptions { this.summary = Summary.none, this.setExitIfChanged = false, List? experimentFlags, - }) : experimentFlags = [...?experimentFlags]; + List? excludePatterns, + }) : experimentFlags = [...?experimentFlags], + excludePatterns = [...?excludePatterns]; /// Called when [file] is about to be formatted. /// diff --git a/lib/src/io.dart b/lib/src/io.dart index fe271c9e..504ea2d4 100644 --- a/lib/src/io.dart +++ b/lib/src/io.dart @@ -129,6 +129,28 @@ Future formatPaths(FormatterOptions options, List paths) async { } } +/// Checks if the given [path] matches any of the [excludePatterns]. +bool _isExcluded(String path, List excludePatterns) { + var relativePath = p.relative(path); + for (var pattern in excludePatterns) { + if (relativePath.contains(pattern) || _matchesGlob(relativePath, pattern)) { + return true; + } + } + return false; +} + +/// Simple glob matching for patterns like *.dart or **/generated/**. +bool _matchesGlob(String path, String pattern) { + // For simplicity, support basic wildcards: * and ** + var regexPattern = pattern + .replaceAll('.', '\\.') + .replaceAll('*', '.*') + .replaceAll('**', '.*'); + var regex = RegExp('^$regexPattern\$'); + return regex.hasMatch(path); +} + /// Runs the formatter on every .dart file in [path] (and its subdirectories), /// and replaces them with their formatted output. /// @@ -156,6 +178,9 @@ Future _processDirectory( var parts = p.split(p.relative(entry.path, from: directory.path)); if (parts.any((part) => part.startsWith('.'))) continue; + // Check if the file should be excluded. + if (_isExcluded(entry.path, options.excludePatterns)) continue; + if (!await _processFile( cache, options, diff --git a/test/cli/cli_test.dart b/test/cli/cli_test.dart index 7b0770b9..f1a5d71f 100644 --- a/test/cli/cli_test.dart +++ b/test/cli/cli_test.dart @@ -247,4 +247,91 @@ void main() { await process.shouldExit(70); }); }); + + group('--exclude', () { + test('excludes specific files', () async { + await d.dir('code', [ + d.file('a.dart', unformattedSource), + d.file('b.dart', unformattedSource), + d.file('c.dart', unformattedSource), + ]).create(); + + var process = await runFormatter(['--exclude', 'b.dart', 'code']); + await expectLater( + process.stdout, + emitsInOrder([ + 'Formatted ${p.join('code', 'a.dart')}', + 'Formatted ${p.join('code', 'c.dart')}', + ]), + ); + await expectLater( + process.stdout, + emits(startsWith('Formatted 2 files (2 changed)')), + ); + await process.shouldExit(0); + + // b.dart should remain unformatted. + await d.dir('code', [ + d.file('a.dart', formattedSource), + d.file('b.dart', unformattedSource), + d.file('c.dart', formattedSource), + ]).validate(); + }); + + test('excludes directories', () async { + await d.dir('code', [ + d.dir('subdir', [d.file('a.dart', unformattedSource)]), + d.file('b.dart', unformattedSource), + ]).create(); + + var process = await runFormatter(['--exclude', 'subdir', 'code']); + await expectLater( + process.stdout, + emitsInOrder(['Formatted ${p.join('code', 'b.dart')}']), + ); + await expectLater( + process.stdout, + emits(startsWith('Formatted 1 file (1 changed)')), + ); + await process.shouldExit(0); + + // subdir/a.dart should remain unformatted. + await d.dir('code', [ + d.dir('subdir', [d.file('a.dart', unformattedSource)]), + d.file('b.dart', formattedSource), + ]).validate(); + }); + + test('supports multiple exclude patterns', () async { + await d.dir('code', [ + d.file('a.dart', unformattedSource), + d.file('b.dart', unformattedSource), + d.file('c.dart', unformattedSource), + ]).create(); + + var process = await runFormatter([ + '--exclude', + 'a.dart', + '--exclude', + 'c.dart', + 'code', + ]); + await expectLater( + process.stdout, + emitsInOrder(['Formatted ${p.join('code', 'b.dart')}']), + ); + await expectLater( + process.stdout, + emits(startsWith('Formatted 1 file (1 changed)')), + ); + await process.shouldExit(0); + + // a.dart and c.dart should remain unformatted. + await d.dir('code', [ + d.file('a.dart', unformattedSource), + d.file('b.dart', formattedSource), + d.file('c.dart', unformattedSource), + ]).validate(); + }); + }); }