diff --git a/crates/ruff_cli/src/commands/check.rs b/crates/ruff_cli/src/commands/check.rs index 001cb0ae8449d..7d63e299f4fa1 100644 --- a/crates/ruff_cli/src/commands/check.rs +++ b/crates/ruff_cli/src/commands/check.rs @@ -82,7 +82,7 @@ pub(crate) fn check( let settings = resolver.resolve(path, pyproject_config); - if !resolved_file.is_root() + if (settings.file_resolver.force_exclude || !resolved_file.is_root()) && match_exclusion( resolved_file.path(), resolved_file.file_name(), diff --git a/crates/ruff_cli/src/commands/check_stdin.rs b/crates/ruff_cli/src/commands/check_stdin.rs index 67f01cd0e8d3a..260da5a50135d 100644 --- a/crates/ruff_cli/src/commands/check_stdin.rs +++ b/crates/ruff_cli/src/commands/check_stdin.rs @@ -18,17 +18,19 @@ pub(crate) fn check_stdin( noqa: flags::Noqa, fix_mode: flags::FixMode, ) -> Result { - if let Some(filename) = filename { - if !python_file_at_path(filename, pyproject_config, overrides)? { - return Ok(Diagnostics::default()); - } + if pyproject_config.settings.file_resolver.force_exclude { + if let Some(filename) = filename { + if !python_file_at_path(filename, pyproject_config, overrides)? { + return Ok(Diagnostics::default()); + } - let lint_settings = &pyproject_config.settings.linter; - if filename - .file_name() - .is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude)) - { - return Ok(Diagnostics::default()); + let lint_settings = &pyproject_config.settings.linter; + if filename + .file_name() + .is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude)) + { + return Ok(Diagnostics::default()); + } } } let package_root = filename.and_then(Path::parent).and_then(|path| { diff --git a/crates/ruff_cli/src/commands/format.rs b/crates/ruff_cli/src/commands/format.rs index 2d09472923552..2db1338d4d9b0 100644 --- a/crates/ruff_cli/src/commands/format.rs +++ b/crates/ruff_cli/src/commands/format.rs @@ -117,14 +117,14 @@ pub(crate) fn format( return None; }; - let resolved_settings = resolver.resolve(path, &pyproject_config); + let settings = resolver.resolve(path, &pyproject_config); // Ignore files that are excluded from formatting - if !resolved_file.is_root() + if (settings.file_resolver.force_exclude || !resolved_file.is_root()) && match_exclusion( path, resolved_file.file_name(), - &resolved_settings.formatter.exclude, + &settings.formatter.exclude, ) { return None; @@ -139,13 +139,7 @@ pub(crate) fn format( Some( match catch_unwind(|| { - format_path( - path, - &resolved_settings.formatter, - source_type, - mode, - cache, - ) + format_path(path, &settings.formatter, source_type, mode, cache) }) { Ok(inner) => inner.map(|result| FormatPathResult { path: resolved_file.path().to_path_buf(), diff --git a/crates/ruff_cli/src/commands/format_stdin.rs b/crates/ruff_cli/src/commands/format_stdin.rs index 082dfedfb00da..032ece074ad71 100644 --- a/crates/ruff_cli/src/commands/format_stdin.rs +++ b/crates/ruff_cli/src/commands/format_stdin.rs @@ -31,17 +31,19 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R let mode = FormatMode::from_cli(cli); - if let Some(filename) = cli.stdin_filename.as_deref() { - if !python_file_at_path(filename, &pyproject_config, overrides)? { - return Ok(ExitStatus::Success); - } + if pyproject_config.settings.file_resolver.force_exclude { + if let Some(filename) = cli.stdin_filename.as_deref() { + if !python_file_at_path(filename, &pyproject_config, overrides)? { + return Ok(ExitStatus::Success); + } - let format_settings = &pyproject_config.settings.formatter; - if filename - .file_name() - .is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude)) - { - return Ok(ExitStatus::Success); + let format_settings = &pyproject_config.settings.formatter; + if filename + .file_name() + .is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude)) + { + return Ok(ExitStatus::Success); + } } } diff --git a/crates/ruff_cli/tests/format.rs b/crates/ruff_cli/tests/format.rs index be85d94aec22a..322b72cafb365 100644 --- a/crates/ruff_cli/tests/format.rs +++ b/crates/ruff_cli/tests/format.rs @@ -188,6 +188,73 @@ OTHER = "OTHER" Ok(()) } +#[test] +fn force_exclude() -> Result<()> { + let tempdir = TempDir::new()?; + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + r#" +extend-exclude = ["out"] + +[format] +exclude = ["test.py", "generated.py"] +"#, + )?; + + fs::write( + tempdir.path().join("main.py"), + r#" +from test import say_hy + +if __name__ == "__main__": + say_hy("dear Ruff contributor") +"#, + )?; + + // Excluded file but passed to the CLI directly, should be formatted + let test_path = tempdir.path().join("test.py"); + fs::write( + &test_path, + r#" +def say_hy(name: str): + print(f"Hy {name}")"#, + )?; + + fs::write( + tempdir.path().join("generated.py"), + r#"NUMBERS = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 +] +OTHER = "OTHER" +"#, + )?; + + let out_dir = tempdir.path().join("out"); + fs::create_dir(&out_dir)?; + + fs::write(out_dir.join("a.py"), "a = a")?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .current_dir(tempdir.path()) + .args(["format", "--no-cache", "--force-exclude", "--check", "--config"]) + .arg(ruff_toml.file_name().unwrap()) + // Explicitly pass test.py, should be respect the `format.exclude` when `--force-exclude` is present + .arg(test_path.file_name().unwrap()) + // Format all other files in the directory, should respect the `exclude` and `format.exclude` options + .arg("."), @r###" + success: false + exit_code: 1 + ----- stdout ----- + Would reformat: main.py + 1 file would be reformatted + + ----- stderr ----- + "###); + Ok(()) +} + #[test] fn exclude_stdin() -> Result<()> { let tempdir = TempDir::new()?; @@ -209,6 +276,43 @@ exclude = ["generated.py"] .pass_stdin(r#" from test import say_hy +if __name__ == '__main__': + say_hy("dear Ruff contributor") +"#), @r###" + success: true + exit_code: 0 + ----- stdout ----- + from test import say_hy + + if __name__ == "__main__": + say_hy("dear Ruff contributor") + + ----- stderr ----- + "###); + Ok(()) +} + +#[test] +fn force_exclude_stdin() -> Result<()> { + let tempdir = TempDir::new()?; + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + r#" +extend-select = ["B", "Q"] +ignore = ["Q000", "Q001", "Q002", "Q003"] + +[format] +exclude = ["generated.py"] +"#, + )?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .current_dir(tempdir.path()) + .args(["format", "--config", &ruff_toml.file_name().unwrap().to_string_lossy(), "--stdin-filename", "generated.py", "--force-exclude", "-"]) + .pass_stdin(r#" +from test import say_hy + if __name__ == '__main__': say_hy("dear Ruff contributor") "#), @r###" diff --git a/crates/ruff_cli/tests/lint.rs b/crates/ruff_cli/tests/lint.rs index 5602926b95c4f..f36f612ba8919 100644 --- a/crates/ruff_cli/tests/lint.rs +++ b/crates/ruff_cli/tests/lint.rs @@ -262,9 +262,13 @@ from test import say_hy if __name__ == "__main__": say_hy("dear Ruff contributor") "#), @r###" - success: true - exit_code: 0 + success: false + exit_code: 1 ----- stdout ----- + generated.py:4:16: Q000 [*] Double quotes found but single quotes preferred + generated.py:5:12: Q000 [*] Double quotes found but single quotes preferred + Found 2 errors. + [*] 2 fixable with the `--fix` option. ----- stderr ----- "###); diff --git a/crates/ruff_workspace/src/resolver.rs b/crates/ruff_workspace/src/resolver.rs index 1f57a1504eba3..fc12feaad3ace 100644 --- a/crates/ruff_workspace/src/resolver.rs +++ b/crates/ruff_workspace/src/resolver.rs @@ -483,10 +483,6 @@ pub fn python_file_at_path( pyproject_config: &PyprojectConfig, transformer: &dyn ConfigurationTransformer, ) -> Result { - if !pyproject_config.settings.file_resolver.force_exclude { - return Ok(true); - } - // Normalize the path (e.g., convert from relative to absolute). let path = fs::normalize_path(path);