Skip to content

Commit 786430a

Browse files
carljmGlyphack
authored andcommitted
Support negated patterns in [extend-]per-file-ignores (astral-sh#10852)
Fixes astral-sh#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.
1 parent 2c4d529 commit 786430a

File tree

5 files changed

+130
-19
lines changed

5 files changed

+130
-19
lines changed

crates/ruff/tests/lint.rs

+80
Original file line numberDiff line numberDiff line change
@@ -1168,3 +1168,83 @@ def func():
11681168

11691169
Ok(())
11701170
}
1171+
1172+
/// Per-file selects via ! negation in per-file-ignores
1173+
#[test]
1174+
fn negated_per_file_ignores() -> Result<()> {
1175+
let tempdir = TempDir::new()?;
1176+
let ruff_toml = tempdir.path().join("ruff.toml");
1177+
fs::write(
1178+
&ruff_toml,
1179+
r#"
1180+
[lint.per-file-ignores]
1181+
"!selected.py" = ["RUF"]
1182+
"#,
1183+
)?;
1184+
let selected = tempdir.path().join("selected.py");
1185+
fs::write(selected, "")?;
1186+
let ignored = tempdir.path().join("ignored.py");
1187+
fs::write(ignored, "")?;
1188+
1189+
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
1190+
.args(STDIN_BASE_OPTIONS)
1191+
.arg("--config")
1192+
.arg(&ruff_toml)
1193+
.arg("--select")
1194+
.arg("RUF901")
1195+
.current_dir(&tempdir)
1196+
, @r###"
1197+
success: false
1198+
exit_code: 1
1199+
----- stdout -----
1200+
selected.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
1201+
Found 1 error.
1202+
[*] 1 fixable with the `--fix` option.
1203+
1204+
----- stderr -----
1205+
"###);
1206+
Ok(())
1207+
}
1208+
1209+
#[test]
1210+
fn negated_per_file_ignores_absolute() -> Result<()> {
1211+
let tempdir = TempDir::new()?;
1212+
let ruff_toml = tempdir.path().join("ruff.toml");
1213+
fs::write(
1214+
&ruff_toml,
1215+
r#"
1216+
[lint.per-file-ignores]
1217+
"!src/**.py" = ["RUF"]
1218+
"#,
1219+
)?;
1220+
let src_dir = tempdir.path().join("src");
1221+
fs::create_dir(&src_dir)?;
1222+
let selected = src_dir.join("selected.py");
1223+
fs::write(selected, "")?;
1224+
let ignored = tempdir.path().join("ignored.py");
1225+
fs::write(ignored, "")?;
1226+
1227+
insta::with_settings!({filters => vec![
1228+
// Replace windows paths
1229+
(r"\\", "/"),
1230+
]}, {
1231+
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
1232+
.args(STDIN_BASE_OPTIONS)
1233+
.arg("--config")
1234+
.arg(&ruff_toml)
1235+
.arg("--select")
1236+
.arg("RUF901")
1237+
.current_dir(&tempdir)
1238+
, @r###"
1239+
success: false
1240+
exit_code: 1
1241+
----- stdout -----
1242+
src/selected.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
1243+
Found 1 error.
1244+
[*] 1 fixable with the `--fix` option.
1245+
1246+
----- stderr -----
1247+
"###);
1248+
});
1249+
Ok(())
1250+
}

crates/ruff_linter/src/fs.rs

+23-10
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,37 @@ use crate::registry::RuleSet;
99
/// Create a set with codes matching the pattern/code pairs.
1010
pub(crate) fn ignores_from_path(
1111
path: &Path,
12-
pattern_code_pairs: &[(GlobMatcher, GlobMatcher, RuleSet)],
12+
pattern_code_pairs: &[(GlobMatcher, GlobMatcher, bool, RuleSet)],
1313
) -> RuleSet {
1414
let file_name = path.file_name().expect("Unable to parse filename");
1515
pattern_code_pairs
1616
.iter()
17-
.filter_map(|(absolute, basename, rules)| {
17+
.filter_map(|(absolute, basename, negated, rules)| {
1818
if basename.is_match(file_name) {
19-
debug!(
20-
"Adding per-file ignores for {:?} due to basename match on {:?}: {:?}",
21-
path,
22-
basename.glob().regex(),
23-
rules
24-
);
25-
Some(rules)
19+
if *negated { None } else {
20+
debug!(
21+
"Adding per-file ignores for {:?} due to basename match on {:?}: {:?}",
22+
path,
23+
basename.glob().regex(),
24+
rules
25+
);
26+
Some(rules)
27+
}
2628
} else if absolute.is_match(path) {
29+
if *negated { None } else {
30+
debug!(
31+
"Adding per-file ignores for {:?} due to absolute match on {:?}: {:?}",
32+
path,
33+
absolute.glob().regex(),
34+
rules
35+
);
36+
Some(rules)
37+
}
38+
} else if *negated {
2739
debug!(
28-
"Adding per-file ignores for {:?} due to absolute match on {:?}: {:?}",
40+
"Adding per-file ignores for {:?} due to negated pattern matching neither {:?} nor {:?}: {:?}",
2941
path,
42+
basename.glob().regex(),
3043
absolute.glob().regex(),
3144
rules
3245
);

crates/ruff_linter/src/settings/types.rs

+21-6
Original file line numberDiff line numberDiff line change
@@ -296,13 +296,22 @@ impl CacheKey for FilePatternSet {
296296
pub struct PerFileIgnore {
297297
pub(crate) basename: String,
298298
pub(crate) absolute: PathBuf,
299+
pub(crate) negated: bool,
299300
pub(crate) rules: RuleSet,
300301
}
301302

302303
impl PerFileIgnore {
303-
pub fn new(pattern: String, prefixes: &[RuleSelector], project_root: Option<&Path>) -> Self {
304+
pub fn new(
305+
mut pattern: String,
306+
prefixes: &[RuleSelector],
307+
project_root: Option<&Path>,
308+
) -> Self {
304309
// Rules in preview are included here even if preview mode is disabled; it's safe to ignore disabled rules
305310
let rules: RuleSet = prefixes.iter().flat_map(RuleSelector::all_rules).collect();
311+
let negated = pattern.starts_with('!');
312+
if negated {
313+
pattern.drain(..1);
314+
}
306315
let path = Path::new(&pattern);
307316
let absolute = match project_root {
308317
Some(project_root) => fs::normalize_path_to(path, project_root),
@@ -312,6 +321,7 @@ impl PerFileIgnore {
312321
Self {
313322
basename: pattern,
314323
absolute,
324+
negated,
315325
rules,
316326
}
317327
}
@@ -593,7 +603,7 @@ pub type IdentifierPattern = glob::Pattern;
593603
#[derive(Debug, Clone, CacheKey, Default)]
594604
pub struct PerFileIgnores {
595605
// Ordered as (absolute path matcher, basename matcher, rules)
596-
ignores: Vec<(GlobMatcher, GlobMatcher, RuleSet)>,
606+
ignores: Vec<(GlobMatcher, GlobMatcher, bool, RuleSet)>,
597607
}
598608

599609
impl PerFileIgnores {
@@ -609,7 +619,12 @@ impl PerFileIgnores {
609619
// Construct basename matcher.
610620
let basename = Glob::new(&per_file_ignore.basename)?.compile_matcher();
611621

612-
Ok((absolute, basename, per_file_ignore.rules))
622+
Ok((
623+
absolute,
624+
basename,
625+
per_file_ignore.negated,
626+
per_file_ignore.rules,
627+
))
613628
})
614629
.collect();
615630
Ok(Self { ignores: ignores? })
@@ -622,10 +637,10 @@ impl Display for PerFileIgnores {
622637
write!(f, "{{}}")?;
623638
} else {
624639
writeln!(f, "{{")?;
625-
for (absolute, basename, rules) in &self.ignores {
640+
for (absolute, basename, negated, rules) in &self.ignores {
626641
writeln!(
627642
f,
628-
"\t{{ absolute = {absolute:#?}, basename = {basename:#?}, rules = {rules} }},"
643+
"\t{{ absolute = {absolute:#?}, basename = {basename:#?}, negated = {negated:#?}, rules = {rules} }},"
629644
)?;
630645
}
631646
write!(f, "}}")?;
@@ -635,7 +650,7 @@ impl Display for PerFileIgnores {
635650
}
636651

637652
impl Deref for PerFileIgnores {
638-
type Target = Vec<(GlobMatcher, GlobMatcher, RuleSet)>;
653+
type Target = Vec<(GlobMatcher, GlobMatcher, bool, RuleSet)>;
639654

640655
fn deref(&self) -> &Self::Target {
641656
&self.ignores

crates/ruff_workspace/src/options.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -905,7 +905,8 @@ pub struct LintCommonOptions {
905905

906906
// Tables are required to go last.
907907
/// A list of mappings from file pattern to rule codes or prefixes to
908-
/// exclude, when considering any matching files.
908+
/// exclude, when considering any matching files. An initial '!' negates
909+
/// the file pattern.
909910
#[option(
910911
default = "{}",
911912
value_type = "dict[str, list[RuleSelector]]",
@@ -914,6 +915,8 @@ pub struct LintCommonOptions {
914915
# Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`.
915916
"__init__.py" = ["E402"]
916917
"path/to/file.py" = ["E402"]
918+
# Ignore `D` rules everywhere except for the `src/` directory.
919+
"!src/**.py" = ["F401"]
917920
"#
918921
)]
919922
pub per_file_ignores: Option<FxHashMap<String, Vec<RuleSelector>>>,

ruff.schema.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)