Skip to content

Commit 8dbe2e6

Browse files
committed
feat(formatter): add --check flag for CI validation
This commit introduces a new `--check` flag to the `format` command, designed specifically for use in CI/CD pipelines and other automated environments. When run with `--check`, the formatter will: - Exit with a success code (`0`) if all files are correctly formatted. - Exit with a failure code (`1`) if any file needs formatting changes. - Produce no output to `stdout`, relying solely on the exit code for status. This provides a clean and efficient way to enforce code style without cluttering CI logs with diffs. To support this, the internal formatting pipeline has been refactored to use a `FormatMode` enum (`Format`, `Check`, `DryRun`) instead of a simple boolean for dry runs. Argument conflicts have also been added to ensure that `--check`, `--dry-run`, and `--stdin-input` are mutually exclusive. Signed-off-by: azjezz <[email protected]>
1 parent a0d7d7f commit 8dbe2e6

File tree

4 files changed

+63
-9
lines changed

4 files changed

+63
-9
lines changed

src/commands/args/reporting.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ impl ReportingArgs {
277277
}
278278
}
279279

280-
let changed = utils::apply_update(&change_log, file, fixed_content, self.dry_run)?;
280+
let changed = utils::apply_update(&change_log, file, fixed_content, self.dry_run, false)?;
281281
progress_bar.inc(1);
282282
Ok(changed)
283283
})

src/commands/format.rs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::config::Configuration;
1515
use crate::database;
1616
use crate::error::Error;
1717
use crate::pipeline::format::FormatContext;
18+
use crate::pipeline::format::FormatMode;
1819
use crate::pipeline::format::run_format_pipeline;
1920

2021
/// Represents the `format` command, which is responsible for formatting source files
@@ -35,11 +36,37 @@ pub struct FormatCommand {
3536
#[arg(help = "Format specific files or directories, overriding the source configuration")]
3637
pub path: Vec<PathBuf>,
3738

38-
/// Perform a dry run to check if files are already formatted.
39-
#[arg(long, short = 'd', help = "Check if the source files are already formatted without making changes")]
39+
/// Perform a dry run, printing a diff without modifying files.
40+
///
41+
/// This will calculate and print a diff of any changes that would be made.
42+
/// No files will be modified on disk.
43+
#[arg(
44+
long,
45+
short = 'd',
46+
help = "Print a diff of changes without modifying files",
47+
conflicts_with_all = ["check", "stdin_input"],
48+
)]
4049
pub dry_run: bool,
4150

42-
#[arg(long, short = 'i', help = "Read input from STDIN, format it, and write to STDOUT")]
51+
/// Check if the source files are formatted.
52+
///
53+
/// This flag is ideal for CI environments. The command will exit with a
54+
/// success code (`0`) if all files are formatted, and a failure code (`1`)
55+
/// if any files would be changed. No output is printed to `stdout`.
56+
#[arg(
57+
long,
58+
short = 'c',
59+
help = "Check if files are formatted, exiting with a non-zero status code on changes",
60+
conflicts_with_all = ["dry_run", "stdin_input"],
61+
)]
62+
pub check: bool,
63+
64+
#[arg(
65+
long,
66+
short = 'i',
67+
help = "Read input from STDIN, format it, and write to STDOUT",
68+
conflicts_with_all = ["dry_run", "check", "path"],
69+
)]
4370
pub stdin_input: bool,
4471
}
4572

@@ -84,7 +111,13 @@ pub fn execute(command: FormatCommand, mut configuration: Configuration) -> Resu
84111
let shared_context = FormatContext {
85112
php_version: configuration.php_version,
86113
settings: configuration.formatter.settings,
87-
dry_run: command.dry_run,
114+
mode: if command.dry_run {
115+
FormatMode::DryRun
116+
} else if command.check {
117+
FormatMode::Check
118+
} else {
119+
FormatMode::Format
120+
},
88121
change_log: change_log.clone(),
89122
};
90123

@@ -98,7 +131,7 @@ pub fn execute(command: FormatCommand, mut configuration: Configuration) -> Resu
98131
return Ok(ExitCode::SUCCESS);
99132
}
100133

101-
Ok(if command.dry_run {
134+
Ok(if command.dry_run || command.check {
102135
tracing::info!("Found {} file(s) that need formatting.", changed_count);
103136
ExitCode::FAILURE
104137
} else {

src/pipeline/format.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,25 @@ impl StatelessReducer<bool, usize> for FormatReducer {
2424
}
2525
}
2626

27+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28+
pub enum FormatMode {
29+
/// Apply formatting changes to files.
30+
Format,
31+
/// Check if files are formatted without making changes.
32+
Check,
33+
/// Print a diff of changes without modifying files.
34+
DryRun,
35+
}
36+
2737
/// Shared, read-only context provided to each parallel formatting task.
2838
#[derive(Clone)]
2939
pub struct FormatContext {
3040
/// The target PHP version for formatting rules.
3141
pub php_version: PHPVersion,
3242
/// The configured settings for the formatter.
3343
pub settings: FormatSettings,
34-
/// If `true`, the pipeline will only check for changes and not modify files.
35-
pub dry_run: bool,
44+
/// The mode of operation: format, check, or dry-run.
45+
pub mode: FormatMode,
3646
/// A thread-safe log for recording formatting changes.
3747
pub change_log: ChangeLog,
3848
}
@@ -69,7 +79,13 @@ pub fn run_format_pipeline(
6979
let formatter = Formatter::new(&interner, context.php_version, context.settings);
7080
let formatted_content = formatter.format(&file, &program);
7181

72-
utils::apply_update(&context.change_log, &file, formatted_content, context.dry_run)
82+
utils::apply_update(
83+
&context.change_log,
84+
&file,
85+
formatted_content,
86+
matches!(context.mode, FormatMode::DryRun),
87+
matches!(context.mode, FormatMode::Check),
88+
)
7389
},
7490
)
7591
}

src/utils/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,16 @@ pub fn apply_update(
3333
file: &File,
3434
modified_contents: String,
3535
dry_run: bool,
36+
check: bool,
3637
) -> Result<bool, Error> {
3738
if file.contents == modified_contents {
3839
return Ok(false);
3940
}
4041

42+
if check {
43+
return Ok(true);
44+
}
45+
4146
if dry_run {
4247
let patch = diffy::create_patch(&file.contents, modified_contents.as_str());
4348

0 commit comments

Comments
 (0)