Respect --force-exclude for lint.exclude and format.exclude (#8393)

## Summary

We typically avoid enforcing exclusions if a file was passed to Ruff
directly on the CLI. However, we also allow `--force-exclude`, which
ignores excluded files _even_ if they're passed to Ruff directly. This
is really important for pre-commit, which always passes changed files --
we need to exclude files passed by pre-commit if they're in the
`exclude` lists.

Turns out the new `lint.exclude` and `format.exclude` settings weren't
respecting `--force-exclude`.

Closes https://github.com/astral-sh/ruff/issues/8391.
This commit is contained in:
Charlie Marsh 2023-10-31 14:45:48 -07:00 committed by GitHub
parent 38358980f1
commit 1642f4dbd9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 139 additions and 37 deletions

View file

@ -82,7 +82,7 @@ pub(crate) fn check(
let settings = resolver.resolve(path, pyproject_config); let settings = resolver.resolve(path, pyproject_config);
if !resolved_file.is_root() if (settings.file_resolver.force_exclude || !resolved_file.is_root())
&& match_exclusion( && match_exclusion(
resolved_file.path(), resolved_file.path(),
resolved_file.file_name(), resolved_file.file_name(),

View file

@ -18,6 +18,7 @@ pub(crate) fn check_stdin(
noqa: flags::Noqa, noqa: flags::Noqa,
fix_mode: flags::FixMode, fix_mode: flags::FixMode,
) -> Result<Diagnostics> { ) -> Result<Diagnostics> {
if pyproject_config.settings.file_resolver.force_exclude {
if let Some(filename) = filename { if let Some(filename) = filename {
if !python_file_at_path(filename, pyproject_config, overrides)? { if !python_file_at_path(filename, pyproject_config, overrides)? {
return Ok(Diagnostics::default()); return Ok(Diagnostics::default());
@ -31,6 +32,7 @@ pub(crate) fn check_stdin(
return Ok(Diagnostics::default()); return Ok(Diagnostics::default());
} }
} }
}
let package_root = filename.and_then(Path::parent).and_then(|path| { let package_root = filename.and_then(Path::parent).and_then(|path| {
packaging::detect_package_root(path, &pyproject_config.settings.linter.namespace_packages) packaging::detect_package_root(path, &pyproject_config.settings.linter.namespace_packages)
}); });

View file

@ -117,14 +117,14 @@ pub(crate) fn format(
return None; return None;
}; };
let resolved_settings = resolver.resolve(path, &pyproject_config); let settings = resolver.resolve(path, &pyproject_config);
// Ignore files that are excluded from formatting // Ignore files that are excluded from formatting
if !resolved_file.is_root() if (settings.file_resolver.force_exclude || !resolved_file.is_root())
&& match_exclusion( && match_exclusion(
path, path,
resolved_file.file_name(), resolved_file.file_name(),
&resolved_settings.formatter.exclude, &settings.formatter.exclude,
) )
{ {
return None; return None;
@ -139,13 +139,7 @@ pub(crate) fn format(
Some( Some(
match catch_unwind(|| { match catch_unwind(|| {
format_path( format_path(path, &settings.formatter, source_type, mode, cache)
path,
&resolved_settings.formatter,
source_type,
mode,
cache,
)
}) { }) {
Ok(inner) => inner.map(|result| FormatPathResult { Ok(inner) => inner.map(|result| FormatPathResult {
path: resolved_file.path().to_path_buf(), path: resolved_file.path().to_path_buf(),

View file

@ -31,6 +31,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
let mode = FormatMode::from_cli(cli); let mode = FormatMode::from_cli(cli);
if pyproject_config.settings.file_resolver.force_exclude {
if let Some(filename) = cli.stdin_filename.as_deref() { if let Some(filename) = cli.stdin_filename.as_deref() {
if !python_file_at_path(filename, &pyproject_config, overrides)? { if !python_file_at_path(filename, &pyproject_config, overrides)? {
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
@ -44,6 +45,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
} }
}
let path = cli.stdin_filename.as_deref(); let path = cli.stdin_filename.as_deref();

View file

@ -188,6 +188,73 @@ OTHER = "OTHER"
Ok(()) 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] #[test]
fn exclude_stdin() -> Result<()> { fn exclude_stdin() -> Result<()> {
let tempdir = TempDir::new()?; let tempdir = TempDir::new()?;
@ -209,6 +276,43 @@ exclude = ["generated.py"]
.pass_stdin(r#" .pass_stdin(r#"
from test import say_hy 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__': if __name__ == '__main__':
say_hy("dear Ruff contributor") say_hy("dear Ruff contributor")
"#), @r###" "#), @r###"

View file

@ -262,9 +262,13 @@ from test import say_hy
if __name__ == "__main__": if __name__ == "__main__":
say_hy("dear Ruff contributor") say_hy("dear Ruff contributor")
"#), @r###" "#), @r###"
success: true success: false
exit_code: 0 exit_code: 1
----- stdout ----- ----- 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 ----- ----- stderr -----
"###); "###);

View file

@ -483,10 +483,6 @@ pub fn python_file_at_path(
pyproject_config: &PyprojectConfig, pyproject_config: &PyprojectConfig,
transformer: &dyn ConfigurationTransformer, transformer: &dyn ConfigurationTransformer,
) -> Result<bool> { ) -> Result<bool> {
if !pyproject_config.settings.file_resolver.force_exclude {
return Ok(true);
}
// Normalize the path (e.g., convert from relative to absolute). // Normalize the path (e.g., convert from relative to absolute).
let path = fs::normalize_path(path); let path = fs::normalize_path(path);