Skip to content

Conversation

@amyreese
Copy link
Member

@amyreese amyreese commented Dec 4, 2025

Adds new diagnostics for reporting unused range suppressions.

  • Updates RUF100 to accept an enum to determine whether the diagnostic mentions noqa or "suppression" in the description and fix title.
  • Updates existing noqa logic to pass the appropriate enum when reporting RUF100 diagnostics.
  • Adds new logic to the new ranged Suppressions system to track whether individual suppressions/codes have been "used", and then report appropriat RUF100 diagnostics for unused suppressions/codes.
  • Adds new integration tests exercising a variety of unused suppression cases.

Issue #3711

@amyreese amyreese changed the base branch from main to amy/suppress-diagnostics December 4, 2025 02:21
@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 4, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

/// - [Ruff error suppression](https://docs.astral.sh/ruff/linter/#error-suppression)
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "0.14.9")]
pub(crate) struct UnusedSuppression;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you create a new rule because that was easier or do you forsee use cases where users want to disable unused block level suppression warnings but not warnings about unused noqa comments?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having two rules might be confusing to users. I can see how the old rule name isn't great, but the name of this rule is generic enough that a user could think that it also handles noqa comments.

Given that our long-term plan is to merge all suppression comments, I'd lean towards reusing the old rule and renaming it later.

parsed.has_valid_syntax(),
settings,
&suppressions,
&mut suppressions,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just pass suppressions here (the owned value), given that they aren't used afterwards?

Another alternative would be to store them on LintContext (I don't remember if that would be hard. It's okay to only look at this at the very end. Just floating this as an idea). Ultimately, I think, where we store it probably depends on where we want to use it, e.g. e might need to pass them to a new lint rule that checks for unclosed suppressions)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back and forth a couple times on what pieces should own the creation of the Suppressions object. The biggest problem is that the existing APIs are very inconsistent on what's available where, like how far context/locator/tokens get passed through. I like the idea of adding them to LintContext but that context isn't passed to generate_noqa_edits for example.

@amyreese amyreese force-pushed the amy/unused-suppressions branch from 14b1a4d to bc990f1 Compare December 5, 2025 02:23
@amyreese amyreese marked this pull request as ready for review December 5, 2025 16:20
@amyreese
Copy link
Member Author

amyreese commented Dec 5, 2025

Not particularly happy with &mut everywhere, but not sure what other options I have, especially now that we're passing in suppressions objects from further up the stack.

@MichaReiser
Copy link
Member

Not particularly happy with &mut everywhere, but not sure what other options I have, especially now that we're passing in suppressions objects from further up the stack.

One option you have is to use interior mutability by making Suppression::used a Cell<bool> or AtomicBool.

I don't mind the &mut overly much, but this is also a case where using iterior mutability seems a great fit because the fact that marking suppressions as used requires mutating isn't something that API users should really be aware of and using Cell<bool> allows you to hide that.

Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation looks great.

We should add some tests for the new rule that show that the fixes are working as expected. This is the reason why I'm requesting changes.

We should also add some tests demonstrating how you can use ruff:disable to suppress unused-suppression errors within a block.

I'm also leaning towards reusing the existing rules but I'm curious to learn about your motivation for having different rules (and how that expands to the other checks we want to add)

Comment on lines +185 to +184
let code_index = comment
.codes
.iter()
.position(|range| locator.slice(range) == suppression.code)
.unwrap();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to store the suppression range on Suppression to correctly handle:

# ruff:disable[RUF100, RUF100]

/// - [Ruff error suppression](https://docs.astral.sh/ruff/linter/#error-suppression)
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "0.14.9")]
pub(crate) struct UnusedSuppression;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having two rules might be confusing to users. I can see how the old rule name isn't great, but the name of this rule is generic enough that a user could think that it also handles noqa comments.

Given that our long-term plan is to merge all suppression comments, I'd lean towards reusing the old rule and renaming it later.

@amyreese amyreese force-pushed the amy/unused-suppressions branch from bc990f1 to 646ad15 Compare December 6, 2025 01:05
@amyreese
Copy link
Member Author

amyreese commented Dec 6, 2025

Updated to use Cell<bool> rather than &mut, to reuse the existing UnusedNOQA rule, as well as to report disabled codes and comments with no codes listed. Duplicate codes are not yet handled.

Currently reports a separate diagnostic for each code due to the way the Suppression struct was designed. I did not realize the current NOQA rules combine everything into a single diagnostic, and I did not like my initial attempt to try and create a single diagnostic from the current data model. That said, ruff check --fix does converge on deleting everything unused, including the entire comment if all of the codes are unused, though presumably requires more iterations which is undesirable.

Not sure if it's worth reworking the data model before going further on this, or if I should worry about that later, or if you have a better idea of which direction to go.

@MichaReiser
Copy link
Member

Not sure if it's worth reworking the data model before going further on this, or if I should worry about that later, or if you have a better idea of which direction to go.

That shouldn't be necessary (other than storing the range of the codes?). We use a similar data structure in ty, and we group consecutive unused codes together, which is perfectly fine (it can result in multiple diagnostics if most codes are unused but only creates one diagnostic if all codes are unused)

See

// Is this the first code directly after the `[`?
let includes_first_code = source[..suppression.range.start().to_usize()]
.trim_end()
.ends_with('[');
let mut current = suppression;
let mut unused_codes = Vec::new();
// Group successive codes together into a single diagnostic,
// or report the entire directive if all codes are unused.
while let Some(next) = unused_iter.peek() {
if let SuppressionTarget::Lint(next_lint) = next.target
&& next.comment_range == current.comment_range
&& source[TextRange::new(current.range.end(), next.range.start())]
.chars()
.all(|c| c.is_whitespace() || c == ',')
{
unused_codes.push(next_lint);
current = *next;
unused_iter.next();
} else {
break;
}
}
// Is the last suppression code the last code before the closing `]`.
let includes_last_code = source[current.range.end().to_usize()..]
.trim_start()
.starts_with(']');

We can address this in a follow-up PR (which is also what I did in ty, the initial version didn't support grouping)

@MichaReiser MichaReiser added rule Implementing or modifying a lint rule preview Related to preview mode features labels Dec 6, 2025
Comment on lines +181 to +279
help: Remove unused suppression
45 | # logged to user
46 | # ruff: disable[E501]
47 | I = 1
- # ruff: enable[E501]
48 |
49 |
50 | def f():

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add some more tests demonstrating some edge cases:

  • Duplicate rule codes (it's okay to add a todo)
  • Disabling RUF100
  • Overlapping ranges (ruff: disable[E501 followed by another ruff: disable[E501])
  • Multiple codes where only one code is unused
  • Multiple codes where only some codes are unused
  • Multiple codes were all codes are unused

Base automatically changed from amy/suppress-diagnostics to main December 9, 2025 00:12
@amyreese amyreese force-pushed the amy/unused-suppressions branch from 646ad15 to 2f8d4a4 Compare December 9, 2025 02:12


def f():
# TODO: Duplicate codes should be counted as duplicate, not unused
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine if one of them counts as unused, for as long as we remove the right one.

@amyreese amyreese merged commit 4e4d018 into main Dec 9, 2025
37 checks passed
@amyreese amyreese deleted the amy/unused-suppressions branch December 9, 2025 16:30
dcreager added a commit that referenced this pull request Dec 9, 2025
…return

* dcreager/die-die-intersections: (29 commits)
  simpler bounds
  [`pylint`] Detect subclasses of builtin exceptions (`PLW0133`) (#21382)
  Fix stack overflow with recursive generic protocols (depth limit) (#21858)
  New diagnostics for unused range suppressions (#21783)
  [ty] Use default settings in completion tests
  [ty] Infer type variables within generic unions  (#21862)
  [ty] Fix overload filtering to prefer more "precise" match (#21859)
  [ty] Stabilize auto-import
  [ty] Fix reveal-type E2E test (#21865)
  [ty] Use concise message for LSP clients not supporting related diagnostic information (#21850)
  Include more details in Tokens 'offset is inside token' panic message (#21860)
  apply range suppressions to filter diagnostics (#21623)
  [ty] followup: add-import action for `reveal_type` too (#21668)
  [ty] Enrich function argument auto-complete suggestions with annotated types
  [ty] Add autocomplete suggestions for function arguments
  [`flake8-bugbear`] Accept immutable slice default arguments (`B008`) (#21823)
  [`pydocstyle`] Suppress `D417` for parameters with `Unpack` annotations (#21816)
  [ty] Remove legacy `concise_message` fallback behavior (#21847)
  [ty] Make Python-version subdiagnostics less verbose (#21849)
  [ty] Supress inlay hints when assigning a trivial initializer call (#21848)
  ...
dcreager added a commit that referenced this pull request Dec 9, 2025
…return

* dcreager/die-die-intersections: (31 commits)
  clippy
  reword comment
  simpler bounds
  [`pylint`] Detect subclasses of builtin exceptions (`PLW0133`) (#21382)
  Fix stack overflow with recursive generic protocols (depth limit) (#21858)
  New diagnostics for unused range suppressions (#21783)
  [ty] Use default settings in completion tests
  [ty] Infer type variables within generic unions  (#21862)
  [ty] Fix overload filtering to prefer more "precise" match (#21859)
  [ty] Stabilize auto-import
  [ty] Fix reveal-type E2E test (#21865)
  [ty] Use concise message for LSP clients not supporting related diagnostic information (#21850)
  Include more details in Tokens 'offset is inside token' panic message (#21860)
  apply range suppressions to filter diagnostics (#21623)
  [ty] followup: add-import action for `reveal_type` too (#21668)
  [ty] Enrich function argument auto-complete suggestions with annotated types
  [ty] Add autocomplete suggestions for function arguments
  [`flake8-bugbear`] Accept immutable slice default arguments (`B008`) (#21823)
  [`pydocstyle`] Suppress `D417` for parameters with `Unpack` annotations (#21816)
  [ty] Remove legacy `concise_message` fallback behavior (#21847)
  ...
dcreager added a commit that referenced this pull request Dec 9, 2025
…return

* dcreager/die-die-intersections: (31 commits)
  clippy
  reword comment
  simpler bounds
  [`pylint`] Detect subclasses of builtin exceptions (`PLW0133`) (#21382)
  Fix stack overflow with recursive generic protocols (depth limit) (#21858)
  New diagnostics for unused range suppressions (#21783)
  [ty] Use default settings in completion tests
  [ty] Infer type variables within generic unions  (#21862)
  [ty] Fix overload filtering to prefer more "precise" match (#21859)
  [ty] Stabilize auto-import
  [ty] Fix reveal-type E2E test (#21865)
  [ty] Use concise message for LSP clients not supporting related diagnostic information (#21850)
  Include more details in Tokens 'offset is inside token' panic message (#21860)
  apply range suppressions to filter diagnostics (#21623)
  [ty] followup: add-import action for `reveal_type` too (#21668)
  [ty] Enrich function argument auto-complete suggestions with annotated types
  [ty] Add autocomplete suggestions for function arguments
  [`flake8-bugbear`] Accept immutable slice default arguments (`B008`) (#21823)
  [`pydocstyle`] Suppress `D417` for parameters with `Unpack` annotations (#21816)
  [ty] Remove legacy `concise_message` fallback behavior (#21847)
  ...
dcreager added a commit that referenced this pull request Dec 10, 2025
* origin/main: (33 commits)
  [ty] Simplify union lower bounds and intersection upper bounds in constraint sets (#21871)
  [ty] Collapse `never` paths in constraint set BDDs (#21880)
  Fix leading comment formatting for lambdas with multiple parameters (#21879)
  [ty] Type inference for `@asynccontextmanager` (#21876)
  Fix comment placement in lambda parameters (#21868)
  [`pylint`] Detect subclasses of builtin exceptions (`PLW0133`) (#21382)
  Fix stack overflow with recursive generic protocols (depth limit) (#21858)
  New diagnostics for unused range suppressions (#21783)
  [ty] Use default settings in completion tests
  [ty] Infer type variables within generic unions  (#21862)
  [ty] Fix overload filtering to prefer more "precise" match (#21859)
  [ty] Stabilize auto-import
  [ty] Fix reveal-type E2E test (#21865)
  [ty] Use concise message for LSP clients not supporting related diagnostic information (#21850)
  Include more details in Tokens 'offset is inside token' panic message (#21860)
  apply range suppressions to filter diagnostics (#21623)
  [ty] followup: add-import action for `reveal_type` too (#21668)
  [ty] Enrich function argument auto-complete suggestions with annotated types
  [ty] Add autocomplete suggestions for function arguments
  [`flake8-bugbear`] Accept immutable slice default arguments (`B008`) (#21823)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Related to preview mode features rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants