mirror of https://github.com/astral-sh/ruff
Show partial fixability indicator in statistics output (#21513)
Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
parent
efb23b01af
commit
df66946b89
|
|
@ -34,9 +34,21 @@ struct ExpandedStatistics<'a> {
|
||||||
code: Option<&'a SecondaryCode>,
|
code: Option<&'a SecondaryCode>,
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
count: usize,
|
count: usize,
|
||||||
fixable: bool,
|
#[serde(rename = "fixable")]
|
||||||
|
all_fixable: bool,
|
||||||
|
fixable_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ExpandedStatistics<'_> {
|
||||||
|
fn any_fixable(&self) -> bool {
|
||||||
|
self.fixable_count > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accumulator type for grouping diagnostics by code.
|
||||||
|
/// Format: (`code`, `representative_diagnostic`, `total_count`, `fixable_count`)
|
||||||
|
type DiagnosticGroup<'a> = (Option<&'a SecondaryCode>, &'a Diagnostic, usize, usize);
|
||||||
|
|
||||||
pub(crate) struct Printer {
|
pub(crate) struct Printer {
|
||||||
format: OutputFormat,
|
format: OutputFormat,
|
||||||
log_level: LogLevel,
|
log_level: LogLevel,
|
||||||
|
|
@ -133,7 +145,7 @@ impl Printer {
|
||||||
if fixables.applicable > 0 {
|
if fixables.applicable > 0 {
|
||||||
writeln!(
|
writeln!(
|
||||||
writer,
|
writer,
|
||||||
"{fix_prefix} {} fixable with the --fix option.",
|
"{fix_prefix} {} fixable with the `--fix` option.",
|
||||||
fixables.applicable
|
fixables.applicable
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
@ -256,35 +268,41 @@ impl Printer {
|
||||||
diagnostics: &Diagnostics,
|
diagnostics: &Diagnostics,
|
||||||
writer: &mut dyn Write,
|
writer: &mut dyn Write,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let required_applicability = self.unsafe_fixes.required_applicability();
|
||||||
let statistics: Vec<ExpandedStatistics> = diagnostics
|
let statistics: Vec<ExpandedStatistics> = diagnostics
|
||||||
.inner
|
.inner
|
||||||
.iter()
|
.iter()
|
||||||
.map(|message| (message.secondary_code(), message))
|
.sorted_by_key(|diagnostic| diagnostic.secondary_code())
|
||||||
.sorted_by_key(|(code, message)| (*code, message.fixable()))
|
.fold(vec![], |mut acc: Vec<DiagnosticGroup>, diagnostic| {
|
||||||
.fold(
|
let is_fixable = diagnostic
|
||||||
vec![],
|
.fix()
|
||||||
|mut acc: Vec<((Option<&SecondaryCode>, &Diagnostic), usize)>, (code, message)| {
|
.is_some_and(|fix| fix.applies(required_applicability));
|
||||||
if let Some(((prev_code, _prev_message), count)) = acc.last_mut() {
|
let code = diagnostic.secondary_code();
|
||||||
|
|
||||||
|
if let Some((prev_code, _prev_message, count, fixable_count)) = acc.last_mut() {
|
||||||
if *prev_code == code {
|
if *prev_code == code {
|
||||||
*count += 1;
|
*count += 1;
|
||||||
|
if is_fixable {
|
||||||
|
*fixable_count += 1;
|
||||||
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
acc.push(((code, message), 1));
|
acc.push((code, diagnostic, 1, usize::from(is_fixable)));
|
||||||
acc
|
acc
|
||||||
},
|
})
|
||||||
)
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&((code, message), count)| ExpandedStatistics {
|
.map(
|
||||||
|
|&(code, message, count, fixable_count)| ExpandedStatistics {
|
||||||
code,
|
code,
|
||||||
name: message.name(),
|
name: message.name(),
|
||||||
count,
|
count,
|
||||||
fixable: if let Some(fix) = message.fix() {
|
// Backward compatibility: `fixable` is true only when all violations are fixable.
|
||||||
fix.applies(self.unsafe_fixes.required_applicability())
|
// See: https://github.com/astral-sh/ruff/pull/21513
|
||||||
} else {
|
all_fixable: fixable_count == count,
|
||||||
false
|
fixable_count,
|
||||||
},
|
},
|
||||||
})
|
)
|
||||||
.sorted_by_key(|statistic| Reverse(statistic.count))
|
.sorted_by_key(|statistic| Reverse(statistic.count))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
@ -308,13 +326,14 @@ impl Printer {
|
||||||
.map(|statistic| statistic.code.map_or(0, |s| s.len()))
|
.map(|statistic| statistic.code.map_or(0, |s| s.len()))
|
||||||
.max()
|
.max()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let any_fixable = statistics.iter().any(|statistic| statistic.fixable);
|
let any_fixable = statistics.iter().any(ExpandedStatistics::any_fixable);
|
||||||
|
|
||||||
let fixable = format!("[{}] ", "*".cyan());
|
let all_fixable = format!("[{}] ", "*".cyan());
|
||||||
|
let partially_fixable = format!("[{}] ", "-".cyan());
|
||||||
let unfixable = "[ ] ";
|
let unfixable = "[ ] ";
|
||||||
|
|
||||||
// By default, we mimic Flake8's `--statistics` format.
|
// By default, we mimic Flake8's `--statistics` format.
|
||||||
for statistic in statistics {
|
for statistic in &statistics {
|
||||||
writeln!(
|
writeln!(
|
||||||
writer,
|
writer,
|
||||||
"{:>count_width$}\t{:<code_width$}\t{}{}",
|
"{:>count_width$}\t{:<code_width$}\t{}{}",
|
||||||
|
|
@ -326,8 +345,10 @@ impl Printer {
|
||||||
.red()
|
.red()
|
||||||
.bold(),
|
.bold(),
|
||||||
if any_fixable {
|
if any_fixable {
|
||||||
if statistic.fixable {
|
if statistic.all_fixable {
|
||||||
&fixable
|
&all_fixable
|
||||||
|
} else if statistic.any_fixable() {
|
||||||
|
&partially_fixable
|
||||||
} else {
|
} else {
|
||||||
unfixable
|
unfixable
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1043,7 +1043,7 @@ def mvce(keys, values):
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
1 C416 [*] unnecessary-comprehension
|
1 C416 [*] unnecessary-comprehension
|
||||||
Found 1 error.
|
Found 1 error.
|
||||||
[*] 1 fixable with the --fix option.
|
[*] 1 fixable with the `--fix` option.
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
");
|
");
|
||||||
|
|
@ -1073,7 +1073,8 @@ def mvce(keys, values):
|
||||||
"code": "C416",
|
"code": "C416",
|
||||||
"name": "unnecessary-comprehension",
|
"name": "unnecessary-comprehension",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixable": false
|
"fixable": false,
|
||||||
|
"fixable_count": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1106,7 +1107,8 @@ def mvce(keys, values):
|
||||||
"code": "C416",
|
"code": "C416",
|
||||||
"name": "unnecessary-comprehension",
|
"name": "unnecessary-comprehension",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"fixable": true
|
"fixable": true,
|
||||||
|
"fixable_count": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1114,6 +1116,54 @@ def mvce(keys, values):
|
||||||
"#);
|
"#);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn show_statistics_json_partial_fix() {
|
||||||
|
let mut cmd = RuffCheck::default()
|
||||||
|
.args([
|
||||||
|
"--select",
|
||||||
|
"UP035",
|
||||||
|
"--statistics",
|
||||||
|
"--output-format",
|
||||||
|
"json",
|
||||||
|
])
|
||||||
|
.build();
|
||||||
|
assert_cmd_snapshot!(cmd
|
||||||
|
.pass_stdin("from typing import List, AsyncGenerator"), @r#"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "UP035",
|
||||||
|
"name": "deprecated-import",
|
||||||
|
"count": 2,
|
||||||
|
"fixable": false,
|
||||||
|
"fixable_count": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn show_statistics_partial_fix() {
|
||||||
|
let mut cmd = RuffCheck::default()
|
||||||
|
.args(["--select", "UP035", "--statistics"])
|
||||||
|
.build();
|
||||||
|
assert_cmd_snapshot!(cmd
|
||||||
|
.pass_stdin("from typing import List, AsyncGenerator"), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
2 UP035 [-] deprecated-import
|
||||||
|
Found 2 errors.
|
||||||
|
[*] 1 fixable with the `--fix` option.
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn show_statistics_syntax_errors() {
|
fn show_statistics_syntax_errors() {
|
||||||
let mut cmd = RuffCheck::default()
|
let mut cmd = RuffCheck::default()
|
||||||
|
|
@ -1810,7 +1860,7 @@ fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
|
||||||
--> -:1:1
|
--> -:1:1
|
||||||
|
|
||||||
Found 2 errors.
|
Found 2 errors.
|
||||||
[*] 1 fixable with the --fix option.
|
[*] 1 fixable with the `--fix` option.
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
");
|
");
|
||||||
|
|
@ -1853,7 +1903,7 @@ fn check_shows_unsafe_fixes_with_opt_in() {
|
||||||
--> -:1:1
|
--> -:1:1
|
||||||
|
|
||||||
Found 2 errors.
|
Found 2 errors.
|
||||||
[*] 2 fixable with the --fix option.
|
[*] 2 fixable with the `--fix` option.
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
");
|
");
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ crates/ruff_linter/resources/test/project/examples/docs/docs/file.py:8:5: F841 [
|
||||||
crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused
|
crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused
|
||||||
crates/ruff_linter/resources/test/project/project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
crates/ruff_linter/resources/test/project/project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
||||||
Found 7 errors.
|
Found 7 errors.
|
||||||
[*] 7 potentially fixable with the --fix option.
|
[*] 7 potentially fixable with the `--fix` option.
|
||||||
```
|
```
|
||||||
|
|
||||||
Running from the project directory itself should exhibit the same behavior:
|
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
|
||||||
project/file.py:1:8: F401 [*] `os` imported but unused
|
project/file.py:1:8: F401 [*] `os` imported but unused
|
||||||
project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
project/import_file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
||||||
Found 7 errors.
|
Found 7 errors.
|
||||||
[*] 7 potentially fixable with the --fix option.
|
[*] 7 potentially fixable with the `--fix` option.
|
||||||
```
|
```
|
||||||
|
|
||||||
Running from the sub-package directory should exhibit the same behavior, but omit the top-level
|
Running from the sub-package directory should exhibit the same behavior, but omit the top-level
|
||||||
|
|
@ -43,7 +43,7 @@ files:
|
||||||
docs/file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
docs/file.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
||||||
docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used
|
docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used
|
||||||
Found 2 errors.
|
Found 2 errors.
|
||||||
[*] 2 potentially fixable with the --fix option.
|
[*] 2 potentially fixable with the `--fix` option.
|
||||||
```
|
```
|
||||||
|
|
||||||
`--config` should force Ruff to use the specified `pyproject.toml` for all files, and resolve
|
`--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
|
||||||
crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused
|
crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused
|
||||||
crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused
|
crates/ruff_linter/resources/test/project/project/file.py:1:8: F401 [*] `os` imported but unused
|
||||||
Found 9 errors.
|
Found 9 errors.
|
||||||
[*] 9 potentially fixable with the --fix option.
|
[*] 9 potentially fixable with the `--fix` option.
|
||||||
```
|
```
|
||||||
|
|
||||||
Running from a parent directory should "ignore" the `exclude` (hence, `concepts/file.py` gets
|
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
|
||||||
docs/docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used
|
docs/docs/file.py:8:5: F841 [*] Local variable `x` is assigned to but never used
|
||||||
excluded/script.py:5:5: F841 [*] Local variable `x` is assigned to but never used
|
excluded/script.py:5:5: F841 [*] Local variable `x` is assigned to but never used
|
||||||
Found 4 errors.
|
Found 4 errors.
|
||||||
[*] 4 potentially fixable with the --fix option.
|
[*] 4 potentially fixable with the `--fix` option.
|
||||||
```
|
```
|
||||||
|
|
||||||
Passing an excluded directory directly should report errors in the contained files:
|
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
|
||||||
∴ cargo run -p ruff -- check crates/ruff_linter/resources/test/project/examples/excluded/
|
∴ cargo run -p ruff -- check crates/ruff_linter/resources/test/project/examples/excluded/
|
||||||
crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused
|
crates/ruff_linter/resources/test/project/examples/excluded/script.py:1:8: F401 [*] `os` imported but unused
|
||||||
Found 1 error.
|
Found 1 error.
|
||||||
[*] 1 potentially fixable with the --fix option.
|
[*] 1 potentially fixable with the `--fix` option.
|
||||||
```
|
```
|
||||||
|
|
||||||
Unless we `--force-exclude`:
|
Unless we `--force-exclude`:
|
||||||
|
|
|
||||||
|
|
@ -454,7 +454,7 @@ Untitled.ipynb:cell_1:2:5: F841 Local variable `x` is assigned to but never used
|
||||||
Untitled.ipynb:cell_2:1:1: E402 Module level import not at top of file
|
Untitled.ipynb:cell_2:1:1: E402 Module level import not at top of file
|
||||||
Untitled.ipynb:cell_2:1:8: F401 `os` imported but unused
|
Untitled.ipynb:cell_2:1:8: F401 `os` imported but unused
|
||||||
Found 3 errors.
|
Found 3 errors.
|
||||||
1 potentially fixable with the --fix option.
|
1 potentially fixable with the `--fix` option.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Does Ruff support NumPy- or Google-style docstrings?
|
## Does Ruff support NumPy- or Google-style docstrings?
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue