diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index a083846cdf00c..e1b3eea62cfba 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -3442,6 +3442,20 @@ impl<'a> LintContext<'a> { guard } + /// Return a [`DiagnosticGuard`] for reporting a diagnostic, with its fix title deferred, if the + /// corresponding rule is enabled. + /// + /// Prefer [`LintContext::report_diagnostic_if_enabled`] unless you need to attach + /// sub-diagnostics before the fix title. See its documentation for more details. + pub(crate) fn report_custom_diagnostic_if_enabled<'chk, T: Violation>( + &'chk self, + kind: T, + range: TextRange, + ) -> Option> { + self.is_rule_enabled(T::rule()) + .then(|| self.report_custom_diagnostic(kind, range)) + } + #[inline] pub(crate) const fn is_rule_enabled(&self, rule: Rule) -> bool { self.rules.enabled(rule) diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs index 94dfd74187e06..038a2d6a46a32 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_rule_code.rs @@ -47,6 +47,11 @@ impl InvalidRuleCodeKind { /// ``` /// /// ## Options +/// +/// This rule will flag rule codes that are unknown to Ruff, even if they are +/// valid for other tools. You can tell Ruff to ignore such codes by configuring +/// the list of known "external" rule codes with the following option: +/// /// - `lint.external` #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "0.15.0")] @@ -120,20 +125,20 @@ fn all_codes_invalid_diagnostic( locator: &Locator, context: &LintContext, ) { - context - .report_diagnostic( - InvalidRuleCode { - rule_code: invalid_codes - .into_iter() - .map(Code::as_str) - .collect::>() - .join(", "), - kind: InvalidRuleCodeKind::Noqa, - whole_comment: true, - }, - directive.range(), - ) - .set_fix(Fix::safe_edit(delete_comment(directive.range(), locator))); + let mut diagnostic = context.report_custom_diagnostic( + InvalidRuleCode { + rule_code: invalid_codes + .into_iter() + .map(Code::as_str) + .collect::>() + .join(", "), + kind: InvalidRuleCodeKind::Noqa, + whole_comment: true, + }, + directive.range(), + ); + diagnostic.set_fix(Fix::safe_edit(delete_comment(directive.range(), locator))); + diagnostic.help("Add non-Ruff rule codes to the `lint.external` configuration option"); } fn some_codes_are_invalid_diagnostic( @@ -142,20 +147,20 @@ fn some_codes_are_invalid_diagnostic( locator: &Locator, context: &LintContext, ) { - context - .report_diagnostic( - InvalidRuleCode { - rule_code: invalid_code.to_string(), - kind: InvalidRuleCodeKind::Noqa, - whole_comment: false, - }, - invalid_code.range(), - ) - .set_fix(Fix::safe_edit(remove_invalid_noqa( - codes, - invalid_code, - locator, - ))); + let mut diagnostic = context.report_custom_diagnostic( + InvalidRuleCode { + rule_code: invalid_code.to_string(), + kind: InvalidRuleCodeKind::Noqa, + whole_comment: false, + }, + invalid_code.range(), + ); + diagnostic.set_fix(Fix::safe_edit(remove_invalid_noqa( + codes, + invalid_code, + locator, + ))); + diagnostic.help("Add non-Ruff rule codes to the `lint.external` configuration option"); } fn remove_invalid_noqa(codes: &Codes, invalid_code: &Code, locator: &Locator) -> Edit { diff --git a/crates/ruff_linter/src/rules/ruff/rules/unused_noqa.rs b/crates/ruff_linter/src/rules/ruff/rules/unused_noqa.rs index 8b8f59d7baadf..cf60acfcb6306 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unused_noqa.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unused_noqa.rs @@ -72,6 +72,11 @@ impl UnusedNOQAKind { /// ``` /// /// ## Options +/// +/// This rule will flag rule codes that are unknown to Ruff, even if they are +/// valid for other tools. You can tell Ruff to ignore such codes by configuring +/// the list of known "external" rule codes with the following option: +/// /// - `lint.external` /// /// ## References diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102.py.snap index 9b338a9ee11d2..7ef5a770bea5a 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102.py.snap @@ -10,6 +10,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123 3 | # External code 4 | import re # noqa: V123 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the `# noqa` comment 1 | # Invalid code - import os # noqa: INVALID123 @@ -28,6 +29,7 @@ RUF102 [*] Invalid rule code in `# noqa`: V123 5 | # Valid noqa 6 | import sys # noqa: E402 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the `# noqa` comment 1 | # Invalid code 2 | import os # noqa: INVALID123 @@ -48,6 +50,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID456 8 | from itertools import product # Preceeding comment # noqa: INVALID789 9 | # Succeeding comment | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `INVALID456` 4 | import re # noqa: V123 5 | # Valid noqa @@ -68,6 +71,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID789 9 | # Succeeding comment 10 | import math # noqa: INVALID000 # Succeeding comment | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the `# noqa` comment 5 | # Valid noqa 6 | import sys # noqa: E402 @@ -88,6 +92,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID000 11 | # Mixed valid and invalid 12 | from typing import List # noqa: F401, INVALID123 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the `# noqa` comment 7 | from functools import cache # Preceeding comment # noqa: F401, INVALID456 8 | from itertools import product # Preceeding comment # noqa: INVALID789 @@ -108,6 +113,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123 13 | # Test for multiple invalid 14 | from collections import defaultdict # noqa: INVALID100, INVALID200, F401 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `INVALID123` 9 | # Succeeding comment 10 | import math # noqa: INVALID000 # Succeeding comment @@ -128,6 +134,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID100 15 | # Test for preserving valid codes when fixing 16 | from itertools import chain # noqa: E402, INVALID300, F401 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `INVALID100` 11 | # Mixed valid and invalid 12 | from typing import List # noqa: F401, INVALID123 @@ -148,6 +155,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID200 15 | # Test for preserving valid codes when fixing 16 | from itertools import chain # noqa: E402, INVALID300, F401 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `INVALID200` 11 | # Mixed valid and invalid 12 | from typing import List # noqa: F401, INVALID123 @@ -168,6 +176,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID300 17 | # Test for mixed code types 18 | import json # noqa: E402, INVALID400, V100 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `INVALID300` 13 | # Test for multiple invalid 14 | from collections import defaultdict # noqa: INVALID100, INVALID200, F401 @@ -188,6 +197,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID400 19 | # Test for rule redirects 20 | import pandas as pd # noqa: TCH002 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `INVALID400` 15 | # Test for preserving valid codes when fixing 16 | from itertools import chain # noqa: E402, INVALID300, F401 @@ -208,6 +218,7 @@ RUF102 [*] Invalid rule code in `# noqa`: V100 19 | # Test for rule redirects 20 | import pandas as pd # noqa: TCH002 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `V100` 15 | # Test for preserving valid codes when fixing 16 | from itertools import chain # noqa: E402, INVALID300, F401 @@ -226,6 +237,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123 22 | import pathlib # noqa: INVALID123 some reason | ^^^^^^^^^^^^^^^^^^ | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the `# noqa` comment 19 | # Test for rule redirects 20 | import pandas as pd # noqa: TCH002 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules.snap index 8b5bd182a3c80..adfd9574f0764 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules.snap @@ -10,6 +10,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123 3 | # External code 4 | import re # noqa: V123 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the `# noqa` comment 1 | # Invalid code - import os # noqa: INVALID123 @@ -28,6 +29,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID456 8 | from itertools import product # Preceeding comment # noqa: INVALID789 9 | # Succeeding comment | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `INVALID456` 4 | import re # noqa: V123 5 | # Valid noqa @@ -48,6 +50,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID789 9 | # Succeeding comment 10 | import math # noqa: INVALID000 # Succeeding comment | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the `# noqa` comment 5 | # Valid noqa 6 | import sys # noqa: E402 @@ -68,6 +71,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID000 11 | # Mixed valid and invalid 12 | from typing import List # noqa: F401, INVALID123 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the `# noqa` comment 7 | from functools import cache # Preceeding comment # noqa: F401, INVALID456 8 | from itertools import product # Preceeding comment # noqa: INVALID789 @@ -88,6 +92,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123 13 | # Test for multiple invalid 14 | from collections import defaultdict # noqa: INVALID100, INVALID200, F401 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `INVALID123` 9 | # Succeeding comment 10 | import math # noqa: INVALID000 # Succeeding comment @@ -108,6 +113,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID100 15 | # Test for preserving valid codes when fixing 16 | from itertools import chain # noqa: E402, INVALID300, F401 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `INVALID100` 11 | # Mixed valid and invalid 12 | from typing import List # noqa: F401, INVALID123 @@ -128,6 +134,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID200 15 | # Test for preserving valid codes when fixing 16 | from itertools import chain # noqa: E402, INVALID300, F401 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `INVALID200` 11 | # Mixed valid and invalid 12 | from typing import List # noqa: F401, INVALID123 @@ -148,6 +155,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID300 17 | # Test for mixed code types 18 | import json # noqa: E402, INVALID400, V100 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `INVALID300` 13 | # Test for multiple invalid 14 | from collections import defaultdict # noqa: INVALID100, INVALID200, F401 @@ -168,6 +176,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID400 19 | # Test for rule redirects 20 | import pandas as pd # noqa: TCH002 | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `INVALID400` 15 | # Test for preserving valid codes when fixing 16 | from itertools import chain # noqa: E402, INVALID300, F401 @@ -186,6 +195,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123 22 | import pathlib # noqa: INVALID123 some reason | ^^^^^^^^^^^^^^^^^^ | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the `# noqa` comment 19 | # Test for rule redirects 20 | import pandas as pd # noqa: TCH002 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap index 5ad92799b364e..5847972908e43 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap @@ -327,6 +327,7 @@ RUF102 [*] Invalid rule code in suppression: YF829 97 | # ruff: enable[YF829] | ----- | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the suppression comment 90 | 91 | def f(): @@ -352,6 +353,7 @@ RUF102 [*] Invalid rule code in suppression: RQW320 | ------ 97 | # ruff: enable[YF829] | +help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the rule code `RQW320` 91 | def f(): 92 | # Unknown rule codes diff --git a/crates/ruff_linter/src/suppression.rs b/crates/ruff_linter/src/suppression.rs index f17b9ec3394d7..ec09f363dcf01 100644 --- a/crates/ruff_linter/src/suppression.rs +++ b/crates/ruff_linter/src/suppression.rs @@ -14,7 +14,7 @@ use ruff_python_trivia::{Cursor, indentation_at_offset}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize, TextSlice}; use smallvec::{SmallVec, smallvec}; -use crate::checkers::ast::LintContext; +use crate::checkers::ast::{DiagnosticGuard, LintContext}; use crate::codes::Rule; use crate::fix::edits::delete_comment; use crate::rule_redirects::get_redirect_target; @@ -220,73 +220,80 @@ impl Suppressions { } pub(crate) fn check_suppressions(&self, context: &LintContext, locator: &Locator) { - let mut grouped_diagnostic: Option<(TextRange, SuppressionDiagnostic)> = None; - let mut unmatched_ranges = FxHashSet::default(); - - let process_pending_diagnostics = - |key: Option, - grouped_diagnostic: &Option<(TextRange, SuppressionDiagnostic)>| - -> bool { - if let Some((group_key, group)) = grouped_diagnostic - && key.is_none_or(|key| key != *group_key) - { - if group.any_invalid() { - Suppressions::report_suppression_codes( - context, - locator, - group.suppression, - &group.invalid_codes, - true, - InvalidRuleCode { - rule_code: group.invalid_codes.iter().join(", "), - kind: InvalidRuleCodeKind::Suppression, - whole_comment: group.suppression.codes().len() - == group.invalid_codes.len(), - }, - ); - } - if group.any_unused() { - let mut codes = group.disabled_codes.clone(); - codes.extend(group.unused_codes.clone()); - Suppressions::report_suppression_codes( - context, - locator, - group.suppression, - &codes, - false, - UnusedNOQA { - codes: Some(UnusedCodes { - disabled: group - .disabled_codes - .iter() - .map(ToString::to_string) - .collect_vec(), - duplicated: group - .duplicated_codes - .iter() - .map(ToString::to_string) - .collect_vec(), - unmatched: group - .unused_codes - .iter() - .map(ToString::to_string) - .collect_vec(), - ..Default::default() - }), - kind: UnusedNOQAKind::Suppression, - }, + fn process_pending_diagnostics( + key: Option, + grouped_diagnostic: Option<&(TextRange, SuppressionDiagnostic)>, + context: &LintContext, + locator: &Locator, + ) -> bool { + if let Some((group_key, group)) = grouped_diagnostic + && key.is_none_or(|key| key != *group_key) + { + if group.any_invalid() { + if let Some(mut diagnostic) = Suppressions::report_suppression_codes( + context, + locator, + group.suppression, + &group.invalid_codes, + true, + InvalidRuleCode { + rule_code: group.invalid_codes.iter().join(", "), + kind: InvalidRuleCodeKind::Suppression, + whole_comment: group.suppression.codes().len() + == group.invalid_codes.len(), + }, + ) { + diagnostic.help( + "Add non-Ruff rule codes to the `lint.external` configuration option", ); } - true - } else { - false } - }; + if group.any_unused() { + let mut codes = group.disabled_codes.clone(); + codes.extend(group.unused_codes.clone()); + Suppressions::report_suppression_codes( + context, + locator, + group.suppression, + &codes, + false, + UnusedNOQA { + codes: Some(UnusedCodes { + disabled: group + .disabled_codes + .iter() + .map(ToString::to_string) + .collect_vec(), + duplicated: group + .duplicated_codes + .iter() + .map(ToString::to_string) + .collect_vec(), + unmatched: group + .unused_codes + .iter() + .map(ToString::to_string) + .collect_vec(), + ..Default::default() + }), + kind: UnusedNOQAKind::Suppression, + }, + ); + } + true + } else { + false + } + } + + let mut grouped_diagnostic: Option<(TextRange, SuppressionDiagnostic)> = None; + let mut unmatched_ranges = FxHashSet::default(); for suppression in &self.valid { let key = suppression.comments.disable_comment().range; - if process_pending_diagnostics(Some(key), &grouped_diagnostic) { + if process_pending_diagnostics(Some(key), grouped_diagnostic.as_ref(), context, locator) + { grouped_diagnostic = None; } @@ -335,7 +342,7 @@ impl Suppressions { } } - process_pending_diagnostics(None, &grouped_diagnostic); + process_pending_diagnostics(None, grouped_diagnostic.as_ref(), context, locator); if context.is_rule_enabled(Rule::InvalidSuppressionComment) { for error in &self.errors { @@ -367,14 +374,14 @@ impl Suppressions { } } - fn report_suppression_codes( - context: &LintContext, + fn report_suppression_codes<'a, 'b, T: Violation>( + context: &'a LintContext<'b>, locator: &Locator, suppression: &Suppression, remove_codes: &[&str], highlight_only_code: bool, kind: T, - ) { + ) -> Option> { let disable_comment = suppression.comments.disable_comment(); let (range, edit) = Suppressions::delete_codes_or_comment( locator, @@ -382,7 +389,7 @@ impl Suppressions { remove_codes, highlight_only_code, ); - if let Some(mut diagnostic) = context.report_diagnostic_if_enabled(kind, range) { + if let Some(mut diagnostic) = context.report_custom_diagnostic_if_enabled(kind, range) { if let Some(enable_comment) = suppression.comments.enable_comment() { let (enable_range, enable_range_edit) = Suppressions::delete_codes_or_comment( locator, @@ -395,7 +402,9 @@ impl Suppressions { } else { diagnostic.set_fix(Fix::safe_edit(edit)); } + return Some(diagnostic); } + None } fn delete_codes_or_comment(