mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 12:55:05 +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(())
|
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,13 +9,14 @@ use crate::registry::RuleSet;
|
||||||
/// Create a set with codes matching the pattern/code pairs.
|
/// Create a set with codes matching the pattern/code pairs.
|
||||||
pub(crate) fn ignores_from_path(
|
pub(crate) fn ignores_from_path(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
pattern_code_pairs: &[(GlobMatcher, GlobMatcher, RuleSet)],
|
pattern_code_pairs: &[(GlobMatcher, GlobMatcher, bool, RuleSet)],
|
||||||
) -> RuleSet {
|
) -> RuleSet {
|
||||||
let file_name = path.file_name().expect("Unable to parse filename");
|
let file_name = path.file_name().expect("Unable to parse filename");
|
||||||
pattern_code_pairs
|
pattern_code_pairs
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(absolute, basename, rules)| {
|
.filter_map(|(absolute, basename, negated, rules)| {
|
||||||
if basename.is_match(file_name) {
|
if basename.is_match(file_name) {
|
||||||
|
if *negated { None } else {
|
||||||
debug!(
|
debug!(
|
||||||
"Adding per-file ignores for {:?} due to basename match on {:?}: {:?}",
|
"Adding per-file ignores for {:?} due to basename match on {:?}: {:?}",
|
||||||
path,
|
path,
|
||||||
|
@ -23,7 +24,9 @@ pub(crate) fn ignores_from_path(
|
||||||
rules
|
rules
|
||||||
);
|
);
|
||||||
Some(rules)
|
Some(rules)
|
||||||
|
}
|
||||||
} else if absolute.is_match(path) {
|
} else if absolute.is_match(path) {
|
||||||
|
if *negated { None } else {
|
||||||
debug!(
|
debug!(
|
||||||
"Adding per-file ignores for {:?} due to absolute match on {:?}: {:?}",
|
"Adding per-file ignores for {:?} due to absolute match on {:?}: {:?}",
|
||||||
path,
|
path,
|
||||||
|
@ -31,6 +34,16 @@ pub(crate) fn ignores_from_path(
|
||||||
rules
|
rules
|
||||||
);
|
);
|
||||||
Some(rules)
|
Some(rules)
|
||||||
|
}
|
||||||
|
} else if *negated {
|
||||||
|
debug!(
|
||||||
|
"Adding per-file ignores for {:?} due to negated pattern matching neither {:?} nor {:?}: {:?}",
|
||||||
|
path,
|
||||||
|
basename.glob().regex(),
|
||||||
|
absolute.glob().regex(),
|
||||||
|
rules
|
||||||
|
);
|
||||||
|
Some(rules)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
@ -296,13 +296,22 @@ impl CacheKey for FilePatternSet {
|
||||||
pub struct PerFileIgnore {
|
pub struct PerFileIgnore {
|
||||||
pub(crate) basename: String,
|
pub(crate) basename: String,
|
||||||
pub(crate) absolute: PathBuf,
|
pub(crate) absolute: PathBuf,
|
||||||
|
pub(crate) negated: bool,
|
||||||
pub(crate) rules: RuleSet,
|
pub(crate) rules: RuleSet,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PerFileIgnore {
|
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
|
// 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 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 path = Path::new(&pattern);
|
||||||
let absolute = match project_root {
|
let absolute = match project_root {
|
||||||
Some(project_root) => fs::normalize_path_to(path, project_root),
|
Some(project_root) => fs::normalize_path_to(path, project_root),
|
||||||
|
@ -312,6 +321,7 @@ impl PerFileIgnore {
|
||||||
Self {
|
Self {
|
||||||
basename: pattern,
|
basename: pattern,
|
||||||
absolute,
|
absolute,
|
||||||
|
negated,
|
||||||
rules,
|
rules,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -593,7 +603,7 @@ pub type IdentifierPattern = glob::Pattern;
|
||||||
#[derive(Debug, Clone, CacheKey, Default)]
|
#[derive(Debug, Clone, CacheKey, Default)]
|
||||||
pub struct PerFileIgnores {
|
pub struct PerFileIgnores {
|
||||||
// Ordered as (absolute path matcher, basename matcher, rules)
|
// Ordered as (absolute path matcher, basename matcher, rules)
|
||||||
ignores: Vec<(GlobMatcher, GlobMatcher, RuleSet)>,
|
ignores: Vec<(GlobMatcher, GlobMatcher, bool, RuleSet)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PerFileIgnores {
|
impl PerFileIgnores {
|
||||||
|
@ -609,7 +619,12 @@ impl PerFileIgnores {
|
||||||
// Construct basename matcher.
|
// Construct basename matcher.
|
||||||
let basename = Glob::new(&per_file_ignore.basename)?.compile_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();
|
.collect();
|
||||||
Ok(Self { ignores: ignores? })
|
Ok(Self { ignores: ignores? })
|
||||||
|
@ -622,10 +637,10 @@ impl Display for PerFileIgnores {
|
||||||
write!(f, "{{}}")?;
|
write!(f, "{{}}")?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(f, "{{")?;
|
writeln!(f, "{{")?;
|
||||||
for (absolute, basename, rules) in &self.ignores {
|
for (absolute, basename, negated, rules) in &self.ignores {
|
||||||
writeln!(
|
writeln!(
|
||||||
f,
|
f,
|
||||||
"\t{{ absolute = {absolute:#?}, basename = {basename:#?}, rules = {rules} }},"
|
"\t{{ absolute = {absolute:#?}, basename = {basename:#?}, negated = {negated:#?}, rules = {rules} }},"
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
write!(f, "}}")?;
|
write!(f, "}}")?;
|
||||||
|
@ -635,7 +650,7 @@ impl Display for PerFileIgnores {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for PerFileIgnores {
|
impl Deref for PerFileIgnores {
|
||||||
type Target = Vec<(GlobMatcher, GlobMatcher, RuleSet)>;
|
type Target = Vec<(GlobMatcher, GlobMatcher, bool, RuleSet)>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.ignores
|
&self.ignores
|
||||||
|
|
|
@ -905,7 +905,8 @@ pub struct LintCommonOptions {
|
||||||
|
|
||||||
// Tables are required to go last.
|
// Tables are required to go last.
|
||||||
/// A list of mappings from file pattern to rule codes or prefixes to
|
/// 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(
|
#[option(
|
||||||
default = "{}",
|
default = "{}",
|
||||||
value_type = "dict[str, list[RuleSelector]]",
|
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`.
|
# Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`.
|
||||||
"__init__.py" = ["E402"]
|
"__init__.py" = ["E402"]
|
||||||
"path/to/file.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>>>,
|
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": {
|
"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,
|
"deprecated": true,
|
||||||
"type": [
|
"type": [
|
||||||
"object",
|
"object",
|
||||||
|
@ -2168,7 +2168,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"per-file-ignores": {
|
"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": [
|
"type": [
|
||||||
"object",
|
"object",
|
||||||
"null"
|
"null"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue