Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions lib/src/cli/format_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ final class FormatCommand extends Command<int> {
'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:');

Expand Down Expand Up @@ -312,6 +319,7 @@ final class FormatCommand extends Command<int> {
var setExitIfChanged = argResults['set-exit-if-changed'] as bool;

var experimentFlags = argResults['enable-experiment'] as List<String>;
var excludePatterns = argResults['exclude'] as List<String>;

// 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.
Expand Down Expand Up @@ -339,6 +347,7 @@ final class FormatCommand extends Command<int> {
summary: summary,
setExitIfChanged: setExitIfChanged,
experimentFlags: experimentFlags,
excludePatterns: excludePatterns,
);

if (argResults.rest.isEmpty) {
Expand Down
7 changes: 6 additions & 1 deletion lib/src/cli/formatter_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ final class FormatterOptions {
/// See dart.dev/go/experiments for details.
final List<String> experimentFlags;

/// Patterns for files and directories to exclude from formatting.
final List<String> excludePatterns;

FormatterOptions({
this.languageVersion,
this.indent = 0,
Expand All @@ -65,7 +68,9 @@ final class FormatterOptions {
this.summary = Summary.none,
this.setExitIfChanged = false,
List<String>? experimentFlags,
}) : experimentFlags = [...?experimentFlags];
List<String>? excludePatterns,
}) : experimentFlags = [...?experimentFlags],
excludePatterns = [...?excludePatterns];

/// Called when [file] is about to be formatted.
///
Expand Down
25 changes: 25 additions & 0 deletions lib/src/io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,28 @@ Future<void> formatPaths(FormatterOptions options, List<String> paths) async {
}
}

/// Checks if the given [path] matches any of the [excludePatterns].
bool _isExcluded(String path, List<String> 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.
///
Expand Down Expand Up @@ -156,6 +178,9 @@ Future<bool> _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,
Expand Down
87 changes: 87 additions & 0 deletions test/cli/cli_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
}