Repository: dprint/dprint (language: rust)

Issue:
Add a flag to ignore the "No files found to format with the specified plugins" error message
At canva we have a CLI tool that helps us run common pre-push validation tasks (think `lint-staged`).
One of these tasks is obviously formatting code!

The problem we've got is that if the user touches a file that has the right extension (eg `.ts`), but is excluded from formatting (eg it's a generated `.ts` file) - dprint will fail with an error `No files found to format with the specified plugins ...`.

This makes it cumbersome for us to integrate dprint into our tool as we either need to:
- encode knowledge of the formattable file list into our tool, or
- handle the failure error code then check the `stderr` logs for the substring, and ignore the failure in that case.

The former is doable via `output-file-paths` - but at our scale it's cumbersome and slow as that command for us takes around 30s to run - whereas the format of a small number of files OFC takes <10s.

I'd love it if we could have a flag like `--no-error-on-ignored` or something to tell dprint "I'm aware the files I'm passing might be ignored and I am okay if you ignore them".


Discussion / hints:
dprint will return the `14` exit code specifically for this purpose: https://dprint.dev/cli/#exit-codes

I believe that could be used here instead of adding a flag?
That is helpful as it's more exact than a substring match of the logs - though we would still need to catch the crash, check for the exit code, and pass on the crash in all other cases.

In an ideal scenario dprint would be able to exit 0 here.
Hello, we have the same issue: if a commit doesn't contain a modification to a file included by `dprint`, we get the error code `14` which blocks our pre-commit hook from passing even though everything went fine.  
We would like to be able to disable this behavior with a cli flag.
@cedric-spinergie it will be added soon. For now, the pre-commit hook can be modified to not fail on exit code 14

I'm thinking `--allow-no-files` similary to git's `--allow-empty`
Thank you!  

One workaround I put in place is to mirror the excludes I configured in `dprint.json` inside my pre-commit config.  
Meaning it won't even try to run `dprint` if there are no files for it to format.  
Just telling about this in case it can help someone else in the meantime.

Fix patch:
diff --git a/crates/development/src/config_helpers.rs b/crates/development/src/config_helpers.rs
index 59bc498e4..61298ae8c 100644
--- a/crates/development/src/config_helpers.rs
+++ b/crates/development/src/config_helpers.rs
@@ -1,6 +1,6 @@
 /// Checks for diagnostics and panics if it finds any.
 pub fn ensure_no_diagnostics<T: std::fmt::Debug>(diagnostics: &[T]) {
-  for diagnostic in diagnostics {
+  if let Some(diagnostic) = diagnostics.first() {
     panic!("Diagnostic error: {:?}", diagnostic);
   }
 }
diff --git a/crates/dprint/src/arg_parser.rs b/crates/dprint/src/arg_parser.rs
index 0db160382..5cd30de84 100644
--- a/crates/dprint/src/arg_parser.rs
+++ b/crates/dprint/src/arg_parser.rs
@@ -62,12 +62,23 @@ pub enum SubCommand {
   #[cfg(target_os = "windows")]
   Hidden(HiddenSubCommand),
 }
+impl SubCommand {
+  pub fn allow_no_files(&self) -> bool {
+    match self {
+      SubCommand::Check(a) => a.allow_no_files,
+      SubCommand::Fmt(a) => a.allow_no_files,
+      SubCommand::OutputFormatTimes(a) => a.allow_no_files,
+      _ => false,
+    }
+  }
+}
 
 #[derive(Debug, PartialEq, Eq)]
 pub struct CheckSubCommand {
   pub patterns: FilePatternArgs,
   pub incremental: Option<bool>,
   pub list_different: bool,
+  pub allow_no_files: bool,
 }
 
 #[derive(Debug, PartialEq, Eq)]
@@ -76,6 +87,7 @@ pub struct FmtSubCommand {
   pub patterns: FilePatternArgs,
   pub incremental: Option<bool>,
   pub enable_stable_format: bool,
+  pub allow_no_files: bool,
 }
 
 #[derive(Debug, PartialEq, Eq)]
@@ -93,6 +105,7 @@ pub struct OutputFilePathsSubCommand {
 #[derive(Debug, PartialEq, Eq)]
 pub struct OutputFormatTimesSubCommand {
   pub patterns: FilePatternArgs,
+  pub allow_no_files: bool,
 }
 
 #[derive(Debug, PartialEq, Eq)]
@@ -168,6 +181,7 @@ fn inner_parse_args<TStdInReader: StdInReader>(args: Vec<String>, std_in_reader:
           patterns: parse_file_patterns(matches)?,
           incremental: parse_incremental(matches),
           enable_stable_format: !matches.get_flag("skip-stable-format"),
+          allow_no_files: matches.get_flag("allow-no-files"),
         })
       }
     }
@@ -175,6 +189,7 @@ fn inner_parse_args<TStdInReader: StdInReader>(args: Vec<String>, std_in_reader:
       patterns: parse_file_patterns(matches)?,
       incremental: parse_incremental(matches),
       list_different: matches.get_flag("list-different"),
+      allow_no_files: matches.get_flag("allow-no-files"),
     }),
     ("init", _) => SubCommand::Config(ConfigSubCommand::Init),
     ("config", matches) => SubCommand::Config(match matches.subcommand().unwrap() {
@@ -192,6 +207,7 @@ fn inner_parse_args<TStdInReader: StdInReader>(args: Vec<String>, std_in_reader:
     ("output-resolved-config", _) => SubCommand::OutputResolvedConfig,
     ("output-format-times", matches) => SubCommand::OutputFormatTimes(OutputFormatTimesSubCommand {
       patterns: parse_file_patterns(matches)?,
+      allow_no_files: matches.get_flag("allow-no-files"),
     }),
     ("version", _) => SubCommand::Version,
     ("license", _) => SubCommand::License,
@@ -386,6 +402,7 @@ EXAMPLES:
             .num_args(0)
             .required(false)
         )
+        .add_allow_no_files_arg()
         .arg(
           Arg::new("skip-stable-format")
             .long("skip-stable-format")
@@ -401,6 +418,7 @@ EXAMPLES:
         .about("Checks for any files that haven't been formatted.")
         .add_resolve_file_path_args()
         .add_incremental_arg()
+        .add_allow_no_files_arg()
         .arg(
           Arg::new("list-different")
             .long("list-different")
@@ -444,6 +462,7 @@ EXAMPLES:
       Command::new("output-format-times")
         .about("Prints the amount of time it takes to format each file. Use this for debugging.")
         .add_resolve_file_path_args()
+        .add_allow_no_files_arg()
     )
     .subcommand(
       Command::new("clear-cache")
@@ -530,6 +549,7 @@ EXAMPLES:
 trait ClapExtensions {
   fn add_resolve_file_path_args(self) -> Self;
   fn add_incremental_arg(self) -> Self;
+  fn add_allow_no_files_arg(self) -> Self;
 }
 
 impl ClapExtensions for clap::Command {
@@ -567,6 +587,17 @@ impl ClapExtensions for clap::Command {
         .require_equals(true),
     )
   }
+
+  fn add_allow_no_files_arg(self) -> Self {
+    use clap::Arg;
+    self.arg(
+      Arg::new("allow-no-files")
+        .long("allow-no-files")
+        .help("Causes dprint to exit with exit code 0 when no files are found instead of exit code 14.")
+        .num_args(0)
+        .required(false),
+    )
+  }
 }
 
 #[cfg(test)]
diff --git a/crates/dprint/src/commands/formatting.rs b/crates/dprint/src/commands/formatting.rs
index 20310c7d7..5a4d9f53a 100644
--- a/crates/dprint/src/commands/formatting.rs
+++ b/crates/dprint/src/commands/formatting.rs
@@ -1035,6 +1035,27 @@ mod test {
     assert_no_files_found(&error, &environment);
   }
 
+  #[test]
+  fn should_not_error_when_no_files_match_allow_no_files_output() {
+    run_allow_no_files_test("fmt");
+    run_allow_no_files_test("check");
+    run_allow_no_files_test("output-format-times");
+
+    fn run_allow_no_files_test(sub_command: &str) {
+      // with
+      {
+        let environment = TestEnvironmentBuilder::with_initialized_remote_wasm_plugin().build();
+        assert!(run_test_cli(vec![sub_command, "--allow-no-files", "**/*.txt"], &environment).is_ok());
+      }
+      // without
+      {
+        let environment = TestEnvironmentBuilder::with_initialized_remote_wasm_plugin().build();
+        let error = run_test_cli(vec![sub_command, "**/*.txt"], &environment).err().unwrap();
+        assert_no_files_found(&error, &environment);
+      }
+    }
+  }
+
   #[track_caller]
   fn assert_no_files_found(error: &TestAppError, _environment: &TestEnvironment) {
     assert_eq!(
diff --git a/crates/dprint/src/resolution.rs b/crates/dprint/src/resolution.rs
index f1d03076c..fcbc89455 100644
--- a/crates/dprint/src/resolution.rs
+++ b/crates/dprint/src/resolution.rs
@@ -365,7 +365,9 @@ impl<TEnvironment: Environment> PluginsScopeAndPathsCollection<TEnvironment> {
     for scope in &self.0 {
       scope.scope.ensure_valid_for_cli_args(cli_args)?;
       if let Some(config) = scope.scope.config.as_ref() {
-        scope.file_paths_by_plugins.ensure_not_empty(&config.base_path)?;
+        if !cli_args.sub_command.allow_no_files() {
+          scope.file_paths_by_plugins.ensure_not_empty(&config.base_path)?;
+        }
       }
     }
     Ok(())
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
index 3c4b81ebf..7737e53e5 100644
--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,3 +1,3 @@
 [toolchain]
-channel = "1.73.0"
+channel = "1.74.0"
 components = ["clippy", "rustfmt"]
diff --git a/website/src/cli.md b/website/src/cli.md
index 162b4bded..de70a471c 100644
--- a/website/src/cli.md
+++ b/website/src/cli.md
@@ -95,7 +95,7 @@ This flag is more useful for one-off commands. It is recommended to use the defa
 - `11` - Configuration resolution error
 - `12` - Plugin resolution error
 - `13` - No plugins found error
-- `14` - No files found error (useful for pre-commit hooks)
+- `14` - No files found error (or suppress to `0` with `--allow-no-files` in dprint >= 0.43)
 - `20` - `dprint check` found non-formatted files
 
 ## Shell completions


Test patch: