Skip to content

Commit df66946

Browse files
tsvikasclaudeMichaReiser
authored
Show partial fixability indicator in statistics output (#21513)
Co-authored-by: Claude <[email protected]> Co-authored-by: Micha Reiser <[email protected]>
1 parent efb23b0 commit df66946

File tree

4 files changed

+112
-41
lines changed

4 files changed

+112
-41
lines changed

crates/ruff/src/printer.rs

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,21 @@ struct ExpandedStatistics<'a> {
3434
code: Option<&'a SecondaryCode>,
3535
name: &'static str,
3636
count: usize,
37-
fixable: bool,
37+
#[serde(rename = "fixable")]
38+
all_fixable: bool,
39+
fixable_count: usize,
3840
}
3941

42+
impl ExpandedStatistics<'_> {
43+
fn any_fixable(&self) -> bool {
44+
self.fixable_count > 0
45+
}
46+
}
47+
48+
/// Accumulator type for grouping diagnostics by code.
49+
/// Format: (`code`, `representative_diagnostic`, `total_count`, `fixable_count`)
50+
type DiagnosticGroup<'a> = (Option<&'a SecondaryCode>, &'a Diagnostic, usize, usize);
51+
4052
pub(crate) struct Printer {
4153
format: OutputFormat,
4254
log_level: LogLevel,
@@ -133,7 +145,7 @@ impl Printer {
133145
if fixables.applicable > 0 {
134146
writeln!(
135147
writer,
136-
"{fix_prefix} {} fixable with the --fix option.",
148+
"{fix_prefix} {} fixable with the `--fix` option.",
137149
fixables.applicable
138150
)?;
139151
}
@@ -256,35 +268,41 @@ impl Printer {
256268
diagnostics: &Diagnostics,
257269
writer: &mut dyn Write,
258270
) -> Result<()> {
271+
let required_applicability = self.unsafe_fixes.required_applicability();
259272
let statistics: Vec<ExpandedStatistics> = diagnostics
260273
.inner
261274
.iter()
262-
.map(|message| (message.secondary_code(), message))
263-
.sorted_by_key(|(code, message)| (*code, message.fixable()))
264-
.fold(
265-
vec![],
266-
|mut acc: Vec<((Option<&SecondaryCode>, &Diagnostic), usize)>, (code, message)| {
267-
if let Some(((prev_code, _prev_message), count)) = acc.last_mut() {
268-
if *prev_code == code {
269-
*count += 1;
270-
return acc;
275+
.sorted_by_key(|diagnostic| diagnostic.secondary_code())
276+
.fold(vec![], |mut acc: Vec<DiagnosticGroup>, diagnostic| {
277+
let is_fixable = diagnostic
278+
.fix()
279+
.is_some_and(|fix| fix.applies(required_applicability));
280+
let code = diagnostic.secondary_code();
281+
282+
if let Some((prev_code, _prev_message, count, fixable_count)) = acc.last_mut() {
283+
if *prev_code == code {
284+
*count += 1;
285+
if is_fixable {
286+
*fixable_count += 1;
271287
}
288+
return acc;
272289
}
273-
acc.push(((code, message), 1));
274-
acc
275-
},
276-
)
290+
}
291+
acc.push((code, diagnostic, 1, usize::from(is_fixable)));
292+
acc
293+
})
277294
.iter()
278-
.map(|&((code, message), count)| ExpandedStatistics {
279-
code,
280-
name: message.name(),
281-
count,
282-
fixable: if let Some(fix) = message.fix() {
283-
fix.applies(self.unsafe_fixes.required_applicability())
284-
} else {
285-
false
295+
.map(
296+
|&(code, message, count, fixable_count)| ExpandedStatistics {
297+
code,
298+
name: message.name(),
299+
count,
300+
// Backward compatibility: `fixable` is true only when all violations are fixable.
301+
// See: https://github.com/astral-sh/ruff/pull/21513
302+
all_fixable: fixable_count == count,
303+
fixable_count,
286304
},
287-
})
305+
)
288306
.sorted_by_key(|statistic| Reverse(statistic.count))
289307
.collect();
290308

@@ -308,13 +326,14 @@ impl Printer {
308326
.map(|statistic| statistic.code.map_or(0, |s| s.len()))
309327
.max()
310328
.unwrap();
311-
let any_fixable = statistics.iter().any(|statistic| statistic.fixable);
329+
let any_fixable = statistics.iter().any(ExpandedStatistics::any_fixable);
312330

313-
let fixable = format!("[{}] ", "*".cyan());
331+
let all_fixable = format!("[{}] ", "*".cyan());
332+
let partially_fixable = format!("[{}] ", "-".cyan());
314333
let unfixable = "[ ] ";
315334

316335
// By default, we mimic Flake8's `--statistics` format.
317-
for statistic in statistics {
336+
for statistic in &statistics {
318337
writeln!(
319338
writer,
320339
"{:>count_width$}\t{:<code_width$}\t{}{}",
@@ -326,8 +345,10 @@ impl Printer {
326345
.red()
327346
.bold(),
328347
if any_fixable {
329-
if statistic.fixable {
330-
&fixable
348+
if statistic.all_fixable {
349+
&all_fixable
350+
} else if statistic.any_fixable() {
351+
&partially_fixable
331352
} else {
332353
unfixable
333354
}

crates/ruff/tests/integration_test.rs

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,7 +1043,7 @@ def mvce(keys, values):
10431043
----- stdout -----
10441044
1 C416 [*] unnecessary-comprehension
10451045
Found 1 error.
1046-
[*] 1 fixable with the --fix option.
1046+
[*] 1 fixable with the `--fix` option.
10471047
10481048
----- stderr -----
10491049
");
@@ -1073,7 +1073,8 @@ def mvce(keys, values):
10731073
"code": "C416",
10741074
"name": "unnecessary-comprehension",
10751075
"count": 1,
1076-
"fixable": false
1076+
"fixable": false,
1077+
"fixable_count": 0
10771078
}
10781079
]
10791080
@@ -1106,14 +1107,63 @@ def mvce(keys, values):
11061107
"code": "C416",
11071108
"name": "unnecessary-comprehension",
11081109
"count": 1,
1109-
"fixable": true
1110+
"fixable": true,
1111+
"fixable_count": 1
11101112
}
11111113
]
11121114
11131115
----- stderr -----
11141116
"#);
11151117
}
11161118

1119+
#[test]
1120+
fn show_statistics_json_partial_fix() {
1121+
let mut cmd = RuffCheck::default()
1122+
.args([
1123+
"--select",
1124+
"UP035",
1125+
"--statistics",
1126+
"--output-format",
1127+
"json",
1128+
])
1129+
.build();
1130+
assert_cmd_snapshot!(cmd
1131+
.pass_stdin("from typing import List, AsyncGenerator"), @r#"
1132+
success: false
1133+
exit_code: 1
1134+
----- stdout -----
1135+
[
1136+
{
1137+
"code": "UP035",
1138+
"name": "deprecated-import",
1139+
"count": 2,
1140+
"fixable": false,
1141+
"fixable_count": 1
1142+
}
1143+
]
1144+
1145+
----- stderr -----
1146+
"#);
1147+
}
1148+
1149+
#[test]
1150+
fn show_statistics_partial_fix() {
1151+
let mut cmd = RuffCheck::default()
1152+
.args(["--select", "UP035", "--statistics"])
1153+
.build();
1154+
assert_cmd_snapshot!(cmd
1155+
.pass_stdin("from typing import List, AsyncGenerator"), @r"
1156+
success: false
1157+
exit_code: 1
1158+
----- stdout -----
1159+
2 UP035 [-] deprecated-import
1160+
Found 2 errors.
1161+
[*] 1 fixable with the `--fix` option.
1162+
1163+
----- stderr -----
1164+
");
1165+
}
1166+
11171167
#[test]
11181168
fn show_statistics_syntax_errors() {
11191169
let mut cmd = RuffCheck::default()
@@ -1810,7 +1860,7 @@ fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
18101860
--> -:1:1
18111861
18121862
Found 2 errors.
1813-
[*] 1 fixable with the --fix option.
1863+
[*] 1 fixable with the `--fix` option.
18141864
18151865
----- stderr -----
18161866
");
@@ -1853,7 +1903,7 @@ fn check_shows_unsafe_fixes_with_opt_in() {
18531903
--> -:1:1
18541904
18551905
Found 2 errors.
1856-
[*] 2 fixable with the --fix option.
1906+
[*] 2 fixable with the `--fix` option.
18571907
18581908
----- stderr -----
18591909
");

crates/ruff_linter/resources/test/project/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ crates/ruff_linter/resources/test/project/examples/docs/docs/file.py:8:5: F841 [
1717
crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused
1818
crates/ruff_linter/resources/test/project/project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
1919
Found 7 errors.
20-
[*] 7 potentially fixable with the --fix option.
20+
[*] 7 potentially fixable with the `--fix` option.
2121
```
2222

2323
Running from the project directory itself should exhibit the same behavior:
@@ -32,7 +32,7 @@ examples/docs/docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but n
3232
project/file.py:1:8: F401 [*] `os` imported but unused
3333
project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
3434
Found 7 errors.
35-
[*] 7 potentially fixable with the --fix option.
35+
[*] 7 potentially fixable with the `--fix` option.
3636
```
3737

3838
Running from the sub-package directory should exhibit the same behavior, but omit the top-level
@@ -43,7 +43,7 @@ files:
4343
docs/file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
4444
docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used
4545
Found 2 errors.
46-
[*] 2 potentially fixable with the --fix option.
46+
[*] 2 potentially fixable with the `--fix` option.
4747
```
4848

4949
`--config` should force Ruff to use the specified `pyproject.toml` for all files, and resolve
@@ -61,7 +61,7 @@ crates/ruff_linter/resources/test/project/examples/docs/docs/file.py:4:27: F401
6161
crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused
6262
crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused
6363
Found 9 errors.
64-
[*] 9 potentially fixable with the --fix option.
64+
[*] 9 potentially fixable with the `--fix` option.
6565
```
6666

6767
Running from a parent directory should "ignore" the `exclude` (hence, `concepts/file.py` gets
@@ -74,7 +74,7 @@ docs/docs/file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
7474
docs/docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used
7575
excluded/script.py:5:5: F841 [*] Local variable `x` is assigned to but never used
7676
Found 4 errors.
77-
[*] 4 potentially fixable with the --fix option.
77+
[*] 4 potentially fixable with the `--fix` option.
7878
```
7979

8080
Passing an excluded directory directly should report errors in the contained files:
@@ -83,7 +83,7 @@ Passing an excluded directory directly should report errors in the contained fil
8383
∴ cargo run -p ruff -- check crates/ruff_linter/resources/test/project/examples/excluded/
8484
crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused
8585
Found 1 error.
86-
[*] 1 potentially fixable with the --fix option.
86+
[*] 1 potentially fixable with the `--fix` option.
8787
```
8888

8989
Unless we `--force-exclude`:

docs/faq.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ Untitled.ipynb:cell_1:2:5: F841 Local variable `x` is assigned to but never used
454454
Untitled.ipynb:cell_2:1:1: E402 Module level import not at top of file
455455
Untitled.ipynb:cell_2:1:8: F401 `os` imported but unused
456456
Found 3 errors.
457-
1 potentially fixable with the --fix option.
457+
1 potentially fixable with the `--fix` option.
458458
```
459459

460460
## Does Ruff support NumPy- or Google-style docstrings?

0 commit comments

Comments
 (0)