mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 18:28:24 +00:00
Support negated patterns in [extend-]per-file-ignores (#10852)
Fixes #3172 ## Summary Allow prefixing [extend-]per-file-ignores patterns with `!` to negate the pattern; listed rules / prefixes will be ignored in all files that don't match the pattern. ## Test Plan Added tests for the feature. Rendered docs and checked rendered output.
This commit is contained in:
parent
42d52ebbec
commit
02e88fdbb1
5 changed files with 130 additions and 19 deletions
|
@ -1168,3 +1168,83 @@ def func():
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Per-file selects via ! negation in per-file-ignores
|
||||
#[test]
|
||||
fn negated_per_file_ignores() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
[lint.per-file-ignores]
|
||||
"!selected.py" = ["RUF"]
|
||||
"#,
|
||||
)?;
|
||||
let selected = tempdir.path().join("selected.py");
|
||||
fs::write(selected, "")?;
|
||||
let ignored = tempdir.path().join("ignored.py");
|
||||
fs::write(ignored, "")?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
.arg("--select")
|
||||
.arg("RUF901")
|
||||
.current_dir(&tempdir)
|
||||
, @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
selected.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negated_per_file_ignores_absolute() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
[lint.per-file-ignores]
|
||||
"!src/**.py" = ["RUF"]
|
||||
"#,
|
||||
)?;
|
||||
let src_dir = tempdir.path().join("src");
|
||||
fs::create_dir(&src_dir)?;
|
||||
let selected = src_dir.join("selected.py");
|
||||
fs::write(selected, "")?;
|
||||
let ignored = tempdir.path().join("ignored.py");
|
||||
fs::write(ignored, "")?;
|
||||
|
||||
insta::with_settings!({filters => vec![
|
||||
// Replace windows paths
|
||||
(r"\\", "/"),
|
||||
]}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
.arg("--select")
|
||||
.arg("RUF901")
|
||||
.current_dir(&tempdir)
|
||||
, @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
src/selected.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -9,24 +9,37 @@ use crate::registry::RuleSet;
|
|||
/// Create a set with codes matching the pattern/code pairs.
|
||||
pub(crate) fn ignores_from_path(
|
||||
path: &Path,
|
||||
pattern_code_pairs: &[(GlobMatcher, GlobMatcher, RuleSet)],
|
||||
pattern_code_pairs: &[(GlobMatcher, GlobMatcher, bool, RuleSet)],
|
||||
) -> RuleSet {
|
||||
let file_name = path.file_name().expect("Unable to parse filename");
|
||||
pattern_code_pairs
|
||||
.iter()
|
||||
.filter_map(|(absolute, basename, rules)| {
|
||||
.filter_map(|(absolute, basename, negated, rules)| {
|
||||
if basename.is_match(file_name) {
|
||||
if *negated { None } else {
|
||||
debug!(
|
||||
"Adding per-file ignores for {:?} due to basename match on {:?}: {:?}",
|
||||
path,
|
||||
basename.glob().regex(),
|
||||
rules
|
||||
);
|
||||
Some(rules)
|
||||
}
|
||||
} else if absolute.is_match(path) {
|
||||
if *negated { None } else {
|
||||
debug!(
|
||||
"Adding per-file ignores for {:?} due to absolute match on {:?}: {:?}",
|
||||
path,
|
||||
absolute.glob().regex(),
|
||||
rules
|
||||
);
|
||||
Some(rules)
|
||||
}
|
||||
} else if *negated {
|
||||
debug!(
|
||||
"Adding per-file ignores for {:?} due to basename match on {:?}: {:?}",
|
||||
"Adding per-file ignores for {:?} due to negated pattern matching neither {:?} nor {:?}: {:?}",
|
||||
path,
|
||||
basename.glob().regex(),
|
||||
rules
|
||||
);
|
||||
Some(rules)
|
||||
} else if absolute.is_match(path) {
|
||||
debug!(
|
||||
"Adding per-file ignores for {:?} due to absolute match on {:?}: {:?}",
|
||||
path,
|
||||
absolute.glob().regex(),
|
||||
rules
|
||||
);
|
||||
|
|
|
@ -296,13 +296,22 @@ impl CacheKey for FilePatternSet {
|
|||
pub struct PerFileIgnore {
|
||||
pub(crate) basename: String,
|
||||
pub(crate) absolute: PathBuf,
|
||||
pub(crate) negated: bool,
|
||||
pub(crate) rules: RuleSet,
|
||||
}
|
||||
|
||||
impl PerFileIgnore {
|
||||
pub fn new(pattern: String, prefixes: &[RuleSelector], project_root: Option<&Path>) -> Self {
|
||||
pub fn new(
|
||||
mut pattern: String,
|
||||
prefixes: &[RuleSelector],
|
||||
project_root: Option<&Path>,
|
||||
) -> Self {
|
||||
// Rules in preview are included here even if preview mode is disabled; it's safe to ignore disabled rules
|
||||
let rules: RuleSet = prefixes.iter().flat_map(RuleSelector::all_rules).collect();
|
||||
let negated = pattern.starts_with('!');
|
||||
if negated {
|
||||
pattern.drain(..1);
|
||||
}
|
||||
let path = Path::new(&pattern);
|
||||
let absolute = match project_root {
|
||||
Some(project_root) => fs::normalize_path_to(path, project_root),
|
||||
|
@ -312,6 +321,7 @@ impl PerFileIgnore {
|
|||
Self {
|
||||
basename: pattern,
|
||||
absolute,
|
||||
negated,
|
||||
rules,
|
||||
}
|
||||
}
|
||||
|
@ -593,7 +603,7 @@ pub type IdentifierPattern = glob::Pattern;
|
|||
#[derive(Debug, Clone, CacheKey, Default)]
|
||||
pub struct PerFileIgnores {
|
||||
// Ordered as (absolute path matcher, basename matcher, rules)
|
||||
ignores: Vec<(GlobMatcher, GlobMatcher, RuleSet)>,
|
||||
ignores: Vec<(GlobMatcher, GlobMatcher, bool, RuleSet)>,
|
||||
}
|
||||
|
||||
impl PerFileIgnores {
|
||||
|
@ -609,7 +619,12 @@ impl PerFileIgnores {
|
|||
// Construct basename matcher.
|
||||
let basename = Glob::new(&per_file_ignore.basename)?.compile_matcher();
|
||||
|
||||
Ok((absolute, basename, per_file_ignore.rules))
|
||||
Ok((
|
||||
absolute,
|
||||
basename,
|
||||
per_file_ignore.negated,
|
||||
per_file_ignore.rules,
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
Ok(Self { ignores: ignores? })
|
||||
|
@ -622,10 +637,10 @@ impl Display for PerFileIgnores {
|
|||
write!(f, "{{}}")?;
|
||||
} else {
|
||||
writeln!(f, "{{")?;
|
||||
for (absolute, basename, rules) in &self.ignores {
|
||||
for (absolute, basename, negated, rules) in &self.ignores {
|
||||
writeln!(
|
||||
f,
|
||||
"\t{{ absolute = {absolute:#?}, basename = {basename:#?}, rules = {rules} }},"
|
||||
"\t{{ absolute = {absolute:#?}, basename = {basename:#?}, negated = {negated:#?}, rules = {rules} }},"
|
||||
)?;
|
||||
}
|
||||
write!(f, "}}")?;
|
||||
|
@ -635,7 +650,7 @@ impl Display for PerFileIgnores {
|
|||
}
|
||||
|
||||
impl Deref for PerFileIgnores {
|
||||
type Target = Vec<(GlobMatcher, GlobMatcher, RuleSet)>;
|
||||
type Target = Vec<(GlobMatcher, GlobMatcher, bool, RuleSet)>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.ignores
|
||||
|
|
|
@ -905,7 +905,8 @@ pub struct LintCommonOptions {
|
|||
|
||||
// Tables are required to go last.
|
||||
/// A list of mappings from file pattern to rule codes or prefixes to
|
||||
/// exclude, when considering any matching files.
|
||||
/// exclude, when considering any matching files. An initial '!' negates
|
||||
/// the file pattern.
|
||||
#[option(
|
||||
default = "{}",
|
||||
value_type = "dict[str, list[RuleSelector]]",
|
||||
|
@ -914,6 +915,8 @@ pub struct LintCommonOptions {
|
|||
# Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`.
|
||||
"__init__.py" = ["E402"]
|
||||
"path/to/file.py" = ["E402"]
|
||||
# Ignore `D` rules everywhere except for the `src/` directory.
|
||||
"!src/**.py" = ["F401"]
|
||||
"#
|
||||
)]
|
||||
pub per_file_ignores: Option<FxHashMap<String, Vec<RuleSelector>>>,
|
||||
|
|
4
ruff.schema.json
generated
4
ruff.schema.json
generated
|
@ -554,7 +554,7 @@
|
|||
]
|
||||
},
|
||||
"per-file-ignores": {
|
||||
"description": "A list of mappings from file pattern to rule codes or prefixes to exclude, when considering any matching files.",
|
||||
"description": "A list of mappings from file pattern to rule codes or prefixes to exclude, when considering any matching files. An initial '!' negates the file pattern.",
|
||||
"deprecated": true,
|
||||
"type": [
|
||||
"object",
|
||||
|
@ -2168,7 +2168,7 @@
|
|||
]
|
||||
},
|
||||
"per-file-ignores": {
|
||||
"description": "A list of mappings from file pattern to rule codes or prefixes to exclude, when considering any matching files.",
|
||||
"description": "A list of mappings from file pattern to rule codes or prefixes to exclude, when considering any matching files. An initial '!' negates the file pattern.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue