diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index d4aa25d1262ee..c14ca3e3a6caf 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -3365,6 +3365,7 @@ impl<'a> LintContext<'a> { ) -> DiagnosticGuard<'chk, 'a> { DiagnosticGuard { context: self, + fix_title: kind.fix_title(), diagnostic: Some(kind.into_diagnostic(range, &self.source_file)), rule: T::rule(), } @@ -3384,6 +3385,7 @@ impl<'a> LintContext<'a> { if self.is_rule_enabled(rule) { Some(DiagnosticGuard { context: self, + fix_title: kind.fix_title(), diagnostic: Some(kind.into_diagnostic(range, &self.source_file)), rule, }) @@ -3447,6 +3449,11 @@ pub(crate) struct DiagnosticGuard<'a, 'b> { /// /// This is always `Some` until the `Drop` (or `defuse`) call. diagnostic: Option, + /// The optional help message to display. + /// + /// This is stored here so that it can be added to the `Diagnostic` in the `Drop` impl and + /// render just above the diff, even if other sub-diagnostics have been added. + fix_title: Option, rule: Rule, } @@ -3592,7 +3599,10 @@ impl Drop for DiagnosticGuard<'_, '_> { return; } - if let Some(diagnostic) = self.diagnostic.take() { + if let Some(mut diagnostic) = self.diagnostic.take() { + if let Some(fix_title) = self.fix_title.take() { + diagnostic.help(fix_title); + } self.context.diagnostics.borrow_mut().push(diagnostic); } } diff --git a/crates/ruff_linter/src/message/mod.rs b/crates/ruff_linter/src/message/mod.rs index e5494432cfcf8..f423577c6df2d 100644 --- a/crates/ruff_linter/src/message/mod.rs +++ b/crates/ruff_linter/src/message/mod.rs @@ -75,10 +75,8 @@ pub fn create_panic_diagnostic(error: &PanicError, path: Option<&Path>) -> Diagn diagnostic } -#[expect(clippy::too_many_arguments)] -pub fn create_lint_diagnostic( +pub fn create_lint_diagnostic( body: B, - suggestion: Option, range: TextRange, fix: Option, parent: Option, @@ -88,7 +86,6 @@ pub fn create_lint_diagnostic( ) -> Diagnostic where B: Display, - S: Display, { let mut diagnostic = Diagnostic::new( DiagnosticId::Lint(LintName::of(rule.into())), @@ -108,10 +105,6 @@ where } diagnostic.annotate(annotation); - if let Some(suggestion) = suggestion { - diagnostic.help(suggestion); - } - if let Some(fix) = fix { diagnostic.set_fix(fix); } @@ -278,9 +271,8 @@ def fibonacci(n): let fib_source = SourceFileBuilder::new("fib.py", fib).finish(); let unused_import_start = TextSize::from(7); - let unused_import = create_lint_diagnostic( + let mut unused_import = create_lint_diagnostic( "`os` imported but unused", - Some("Remove unused import: `os`"), TextRange::new(unused_import_start, TextSize::from(9)), Some(Fix::unsafe_edit(Edit::range_deletion(TextRange::new( TextSize::from(0), @@ -291,11 +283,11 @@ def fibonacci(n): Some(unused_import_start), Rule::UnusedImport, ); + unused_import.help("Remove unused import: `os`"); let unused_variable_start = TextSize::from(94); - let unused_variable = create_lint_diagnostic( + let mut unused_variable = create_lint_diagnostic( "Local variable `x` is assigned to but never used", - Some("Remove assignment to unused variable `x`"), TextRange::new(unused_variable_start, TextSize::from(95)), Some(Fix::unsafe_edit(Edit::deletion( TextSize::from(94), @@ -306,13 +298,13 @@ def fibonacci(n): Some(unused_variable_start), Rule::UnusedVariable, ); + unused_variable.help("Remove assignment to unused variable `x`"); let file_2 = r"if a == 1: pass"; let undefined_name_start = TextSize::from(3); let undefined_name = create_lint_diagnostic( "Undefined name `a`", - Option::<&'static str>::None, TextRange::new(undefined_name_start, TextSize::from(4)), None, None, diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap index e2b954b79f3e5..2e830483cf863 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/snapshots/ruff_linter__rules__flake8_implicit_str_concat__tests__ISC004_ISC004.py.snap @@ -11,8 +11,8 @@ ISC004 [*] Unparenthesized implicit string concatenation in collection | |______________________________________________________________^ 6 | ) | -help: Wrap implicitly concatenated strings in parentheses help: Did you forget a comma? +help: Wrap implicitly concatenated strings in parentheses 1 | facts = ( 2 | "Lobsters have blue blood.", 3 | "The liver is the only human organ that can fully regenerate itself.", @@ -35,8 +35,8 @@ ISC004 [*] Unparenthesized implicit string concatenation in collection | |______________________________________________________________^ 13 | ] | -help: Wrap implicitly concatenated strings in parentheses help: Did you forget a comma? +help: Wrap implicitly concatenated strings in parentheses 8 | facts = [ 9 | "Lobsters have blue blood.", 10 | "The liver is the only human organ that can fully regenerate itself.", @@ -59,8 +59,8 @@ ISC004 [*] Unparenthesized implicit string concatenation in collection | |______________________________________________________________^ 20 | } | -help: Wrap implicitly concatenated strings in parentheses help: Did you forget a comma? +help: Wrap implicitly concatenated strings in parentheses 15 | facts = { 16 | "Lobsters have blue blood.", 17 | "The liver is the only human organ that can fully regenerate itself.", @@ -83,8 +83,8 @@ ISC004 [*] Unparenthesized implicit string concatenation in collection | |_________________________^ 33 | ) | -help: Wrap implicitly concatenated strings in parentheses help: Did you forget a comma? +help: Wrap implicitly concatenated strings in parentheses 27 | } 28 | 29 | facts = ( @@ -108,8 +108,8 @@ ISC004 [*] Unparenthesized implicit string concatenation in collection | |_________________________^ 39 | ] | -help: Wrap implicitly concatenated strings in parentheses help: Did you forget a comma? +help: Wrap implicitly concatenated strings in parentheses 33 | ) 34 | 35 | facts = [ @@ -133,8 +133,8 @@ ISC004 [*] Unparenthesized implicit string concatenation in collection | |_________________________^ 45 | } | -help: Wrap implicitly concatenated strings in parentheses help: Did you forget a comma? +help: Wrap implicitly concatenated strings in parentheses 39 | ] 40 | 41 | facts = { diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF064_RUF064.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF064_RUF064.py.snap index 7d5a58f758412..68b00527fbe6b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF064_RUF064.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF064_RUF064.py.snap @@ -11,9 +11,9 @@ RUF064 [*] Non-octal mode 7 | os.chmod("foo", 0o444) # OK 8 | os.chmod("foo", 7777) # Error | -help: Replace with octal literal info: Current value of 444 (0o674) sets permissions: u=rw-, g=rwx, o=r-- info: Suggested value of 0o444 sets permissions: u=r--, g=r--, o=r-- +help: Replace with octal literal 3 | import os 4 | from pathlib import Path 5 | @@ -34,8 +34,8 @@ RUF064 Non-octal mode 9 | os.chmod("foo", 10000) # Error 10 | os.chmod("foo", 99999) # Error | -help: Replace with octal literal info: Current value of 7777 (0o7141) sets permissions: u=--x, g=r--, o=--x +help: Replace with octal literal RUF064 Non-octal mode --> RUF064.py:9:17 @@ -46,8 +46,8 @@ RUF064 Non-octal mode | ^^^^^ 10 | os.chmod("foo", 99999) # Error | -help: Replace with octal literal info: Current value of 10000 (0o3420) sets permissions: u=r--, g=-w-, o=--- +help: Replace with octal literal RUF064 Non-octal mode --> RUF064.py:10:17 @@ -70,9 +70,9 @@ RUF064 [*] Non-octal mode | ^^^ 13 | os.umask(0o777) # OK | -help: Replace with octal literal info: Current value of 777 (0o1411) sets permissions: u=r--, g=--x, o=--x info: Suggested value of 0o777 sets permissions: u=rwx, g=rwx, o=rwx +help: Replace with octal literal 9 | os.chmod("foo", 10000) # Error 10 | os.chmod("foo", 99999) # Error 11 | @@ -92,9 +92,9 @@ RUF064 [*] Non-octal mode | ^^^ 16 | os.fchmod(0, 0o400) # OK | -help: Replace with octal literal info: Current value of 400 (0o620) sets permissions: u=rw-, g=-w-, o=--- info: Suggested value of 0o400 sets permissions: u=r--, g=---, o=--- +help: Replace with octal literal 12 | os.umask(777) # Error 13 | os.umask(0o777) # OK 14 | @@ -114,9 +114,9 @@ RUF064 [*] Non-octal mode | ^^^ 19 | os.lchmod("foo", 0o755) # OK | -help: Replace with octal literal info: Current value of 755 (0o1363) sets permissions: u=-wx, g=rw-, o=-wx info: Suggested value of 0o755 sets permissions: u=rwx, g=r-x, o=r-x +help: Replace with octal literal 15 | os.fchmod(0, 400) # Error 16 | os.fchmod(0, 0o400) # OK 17 | @@ -136,9 +136,9 @@ RUF064 [*] Non-octal mode | ^^^ 22 | os.mkdir("foo", 0o600) # OK | -help: Replace with octal literal info: Current value of 600 (0o1130) sets permissions: u=--x, g=-wx, o=--- info: Suggested value of 0o600 sets permissions: u=rw-, g=---, o=--- +help: Replace with octal literal 18 | os.lchmod("foo", 755) # Error 19 | os.lchmod("foo", 0o755) # OK 20 | @@ -158,9 +158,9 @@ RUF064 [*] Non-octal mode | ^^^ 25 | os.makedirs("foo", 0o644) # OK | -help: Replace with octal literal info: Current value of 644 (0o1204) sets permissions: u=-w-, g=---, o=r-- info: Suggested value of 0o644 sets permissions: u=rw-, g=r--, o=r-- +help: Replace with octal literal 21 | os.mkdir("foo", 600) # Error 22 | os.mkdir("foo", 0o600) # OK 23 | @@ -180,9 +180,9 @@ RUF064 [*] Non-octal mode | ^^^ 28 | os.mkfifo("foo", 0o640) # OK | -help: Replace with octal literal info: Current value of 640 (0o1200) sets permissions: u=-w-, g=---, o=--- info: Suggested value of 0o640 sets permissions: u=rw-, g=r--, o=--- +help: Replace with octal literal 24 | os.makedirs("foo", 644) # Error 25 | os.makedirs("foo", 0o644) # OK 26 | @@ -202,9 +202,9 @@ RUF064 [*] Non-octal mode | ^^^ 31 | os.mknod("foo", 0o660) # OK | -help: Replace with octal literal info: Current value of 660 (0o1224) sets permissions: u=-w-, g=-w-, o=r-- info: Suggested value of 0o660 sets permissions: u=rw-, g=rw-, o=--- +help: Replace with octal literal 27 | os.mkfifo("foo", 640) # Error 28 | os.mkfifo("foo", 0o640) # OK 29 | @@ -224,9 +224,9 @@ RUF064 [*] Non-octal mode | ^^^ 34 | os.open("foo", os.O_CREAT, 0o644) # OK | -help: Replace with octal literal info: Current value of 644 (0o1204) sets permissions: u=-w-, g=---, o=r-- info: Suggested value of 0o644 sets permissions: u=rw-, g=r--, o=r-- +help: Replace with octal literal 30 | os.mknod("foo", 660) # Error 31 | os.mknod("foo", 0o660) # OK 32 | @@ -246,9 +246,9 @@ RUF064 [*] Non-octal mode | ^^^ 37 | Path("bar").chmod(0o755) # OK | -help: Replace with octal literal info: Current value of 755 (0o1363) sets permissions: u=-wx, g=rw-, o=-wx info: Suggested value of 0o755 sets permissions: u=rwx, g=r-x, o=r-x +help: Replace with octal literal 33 | os.open("foo", os.O_CREAT, 644) # Error 34 | os.open("foo", os.O_CREAT, 0o644) # OK 35 | @@ -267,9 +267,9 @@ RUF064 [*] Non-octal mode | ^^^ 41 | path.chmod(0o755) # OK | -help: Replace with octal literal info: Current value of 755 (0o1363) sets permissions: u=-wx, g=rw-, o=-wx info: Suggested value of 0o755 sets permissions: u=rwx, g=r-x, o=r-x +help: Replace with octal literal 37 | Path("bar").chmod(0o755) # OK 38 | 39 | path = Path("bar") @@ -289,9 +289,9 @@ RUF064 [*] Non-octal mode | ^^^ 44 | dbm.open("db", "r", 0o600) # OK | -help: Replace with octal literal info: Current value of 600 (0o1130) sets permissions: u=--x, g=-wx, o=--- info: Suggested value of 0o600 sets permissions: u=rw-, g=---, o=--- +help: Replace with octal literal 40 | path.chmod(755) # Error 41 | path.chmod(0o755) # OK 42 | @@ -311,9 +311,9 @@ RUF064 [*] Non-octal mode | ^^^ 47 | dbm.gnu.open("db", "r", 0o600) # OK | -help: Replace with octal literal info: Current value of 600 (0o1130) sets permissions: u=--x, g=-wx, o=--- info: Suggested value of 0o600 sets permissions: u=rw-, g=---, o=--- +help: Replace with octal literal 43 | dbm.open("db", "r", 600) # Error 44 | dbm.open("db", "r", 0o600) # OK 45 | @@ -333,9 +333,9 @@ RUF064 [*] Non-octal mode | ^^^ 50 | dbm.ndbm.open("db", "r", 0o600) # OK | -help: Replace with octal literal info: Current value of 600 (0o1130) sets permissions: u=--x, g=-wx, o=--- info: Suggested value of 0o600 sets permissions: u=rw-, g=---, o=--- +help: Replace with octal literal 46 | dbm.gnu.open("db", "r", 600) # Error 47 | dbm.gnu.open("db", "r", 0o600) # OK 48 | @@ -355,9 +355,9 @@ RUF064 [*] Non-octal mode | ^^^ 53 | os.fchmod(0, 493) # 0o755 | -help: Replace with octal literal info: Current value of 256 (0o400) sets permissions: u=r--, g=---, o=--- info: Suggested value of 0o400 sets permissions: u=r--, g=---, o=--- +help: Replace with octal literal 49 | dbm.ndbm.open("db", "r", 600) # Error 50 | dbm.ndbm.open("db", "r", 0o600) # OK 51 | @@ -377,9 +377,9 @@ RUF064 [*] Non-octal mode 54 | 55 | # https://github.com/astral-sh/ruff/issues/19010 | -help: Replace with octal literal info: Current value of 493 (0o755) sets permissions: u=rwx, g=r-x, o=r-x info: Suggested value of 0o755 sets permissions: u=rwx, g=r-x, o=r-x +help: Replace with octal literal 50 | dbm.ndbm.open("db", "r", 0o600) # OK 51 | 52 | os.fchmod(0, 256) # 0o400 @@ -398,8 +398,8 @@ RUF064 [*] Non-octal mode | ^^^ 57 | os.chmod("foo", 0000) # Error | -help: Replace with octal literal info: Current value of 000 (0o000) sets permissions: u=---, g=---, o=--- +help: Replace with octal literal 53 | os.fchmod(0, 493) # 0o755 54 | 55 | # https://github.com/astral-sh/ruff/issues/19010 @@ -419,8 +419,8 @@ RUF064 [*] Non-octal mode 58 | 59 | os.chmod("foo", 0b0) # Error | -help: Replace with octal literal info: Current value of 0000 (0o000) sets permissions: u=---, g=---, o=--- +help: Replace with octal literal 54 | 55 | # https://github.com/astral-sh/ruff/issues/19010 56 | os.chmod("foo", 000) # Error @@ -440,8 +440,8 @@ RUF064 Non-octal mode 60 | os.chmod("foo", 0x0) # Error 61 | os.chmod("foo", 0) # Ok | -help: Replace with octal literal info: Current value of 0b0 (0o000) sets permissions: u=---, g=---, o=--- +help: Replace with octal literal RUF064 Non-octal mode --> RUF064.py:60:17 @@ -451,5 +451,5 @@ RUF064 Non-octal mode | ^^^ 61 | os.chmod("foo", 0) # Ok | -help: Replace with octal literal info: Current value of 0x0 (0o000) sets permissions: u=---, g=---, o=--- +help: Replace with octal literal diff --git a/crates/ruff_linter/src/violation.rs b/crates/ruff_linter/src/violation.rs index dc77c99bdd0df..f794710c8cc8e 100644 --- a/crates/ruff_linter/src/violation.rs +++ b/crates/ruff_linter/src/violation.rs @@ -71,7 +71,6 @@ pub trait Violation: ViolationMetadata + Sized { fn into_diagnostic(self, range: TextRange, file: &SourceFile) -> Diagnostic { create_lint_diagnostic( self.message(), - self.fix_title(), range, None, None,