Skip to content

Implement #ruff:ignore logical-line suppressions#23404

Merged
amyreese merged 22 commits intomainfrom
amy/ruff-ignore-end-of-line
Apr 20, 2026
Merged

Implement #ruff:ignore logical-line suppressions#23404
amyreese merged 22 commits intomainfrom
amy/ruff-ignore-end-of-line

Conversation

@amyreese
Copy link
Copy Markdown
Contributor

@amyreese amyreese commented Feb 18, 2026

Adds support for #ruff:ignore[code] style suppressions as either own-line
or end-of-line comments. The range covered by these suppressions is determined
by the comment's position relative to the associated logical line (statement
or suite header).

Standalone ignore comments apply to an entire multi-line statement/header if the comment appears above the first line:

# covers the entire header
def foo(
	arg1,
	arg2,
):
	pass

But will only apply to a single following line if it appears in the middle of a multi-line statement/header:

def foo(
	# only covers the next line
	arg1,
	arg2,
):
	pass

Trailing comments will only apply to a single physical line, similar to existing #noqa comments:

def foo(
    arg1, # only covers arg1
    arg2,
):
    pass

Intervening comments are allowed, which enables "stacking" of #ruff:ignore comments with other own-line pragma comments:

# ruff:ignore[code]
# fmt:off
value = [
	1, 2,
	3, 4,
]
# fmt:on

Includes some refactoring of the structs to generalize the naming/terms used, otherwise the rest of the suppression system should be able to stay unchanged.

to do:

  • documentation
  • more test cases covering stacked comments
  • preview mode gating?

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Feb 18, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@amyreese amyreese added suppression Related to supression of violations e.g. noqa preview Related to preview mode features labels Feb 20, 2026
@amyreese
Copy link
Copy Markdown
Contributor Author

Open question:

  • Should this be gated by preview mode, and if so, should it show a warning of some sort if it finds a #ruff:ignore comment but preview mode is disabled?

@amyreese amyreese force-pushed the amy/ruff-ignore-end-of-line branch from ba3586f to e4a49f4 Compare February 20, 2026 01:52
@amyreese amyreese requested a review from ntBre February 20, 2026 01:52
@amyreese amyreese marked this pull request as ready for review February 20, 2026 18:19
@ntBre
Copy link
Copy Markdown
Contributor

ntBre commented Feb 23, 2026

Am I interpreting the summary correctly that this case:

# covers the entire header
def foo(
	arg1,
	arg2,
):
	pass

only covers the header, as in diagnostics in the range of def foo(arg1, arg2): and not the body of the function?

I think in Rust these kinds of suppressions apply to the whole body of a block, so I'd expect this to apply to the whole function, with similar behavior for other statements with bodies, like if and loops.

I haven't looked closely at the diff yet, but this seems like it could simplify the implementation too if the suppression just applied to the statement starting on the next line.

@amyreese
Copy link
Copy Markdown
Contributor Author

Am I interpreting the summary correctly that this case:

# covers the entire header
def foo(
	arg1,
	arg2,
):
	pass

only covers the header, as in diagnostics in the range of def foo(arg1, arg2): and not the body of the function?

That's correct. It's meant to cover what Python considers one "logical line". I'd personally argue against having it cover the entire body, because that would overlap more with the disable/enable suppressions, and again leave ruff with no way to cover just a multi-line header without needing an end-of-line suppression on every line.

Copy link
Copy Markdown
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

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

This makes sense to me overall, just a few minor suggestions. @dylwil3 might also have some ideas/familiarity with edge cases in the comment handling.

Comment thread docs/linter.md Outdated
#### Line-level

Ruff supports a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html).
To ignore one or more violations within a single "logical" line (a statement or
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd be curious to get your thoughts on the preview gating. If we make this a preview change, I think this should probably be a bit less prominent in the docs. For example, I'd probably leave the existing noqa docs here at the top of the section and put this at the end after In preview, ....

It doesn't feel like this necessarily has to be a preview change since it's a new feature and thus not breaking any existing code, but I wonder if we're going to want to make substantial changes to the syntax or to the exact semantics before we consider it stable. For that reason, I'd still lean toward doing this in preview.

I'd also love to get @MichaReiser's input on this before landing it since he'll be back next week.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree with @ntBre's comment. I'd also be fine to split the docs out of this PR to unblock the implementation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I did move the docs below the #noqa docs in a previous update when adding the preview gating. If there's something else you think needs to be updated with the docs, I'm happy to do that.

Comment thread docs/linter.md Outdated
Comment thread crates/ruff_linter/src/suppression.rs Outdated
Comment thread crates/ruff_linter/src/suppression.rs
Comment thread crates/ruff_linter/src/suppression.rs
Comment thread crates/ruff_linter/src/suppression.rs
Comment thread crates/ruff_linter/src/suppression.rs Outdated
@ntBre
Copy link
Copy Markdown
Contributor

ntBre commented Feb 23, 2026

That's correct. It's meant to cover what Python considers one "logical line". I'd personally argue against having it cover the entire body, because that would overlap more with the disable/enable suppressions, and again leave ruff with no way to cover just a multi-line header without needing an end-of-line suppression on every line.

I guess for me I'd feel more likely to want to suppress something for the whole block than just for the header, so I'd rather have:

# ruff: ignore[...]
def foo():
    stmt1
    stm2
    ...

have the semantics of:

# ruff: disable[...]
def foo():
    stmt1
    stm2
    ...
# ruff: enable[...]

instead of

# ruff: disable[...]
def foo():
# ruff: enable[...]
    stmt1
    stm2
    ...

So they both overlap with range suppressions (or am I missing something with range suppressions since you said "leave ruff with no way to cover just a multi-line header"), but the former seems more useful to me, although I can also see the argument for the latter since it's a narrower range.

Functions might be a bit of a special case since many of their diagnostics are specific to the header. For classes, for example, you might want to suppress invalid-function-name (N802) for the whole class body.

@amyreese
Copy link
Copy Markdown
Contributor Author

There's a couple problems with this example:

# ruff:disable[code]
def foo():
# ruff:enable[code]
	pass

First, it's a bit ugly and non-pythonic, and makes it harder to visually distinguish the function header. Second, the formatter will not let that stay that way — it will indent that second comment to match the indentation of the function body. Third, once that enable comment is indented, we no longer match it with the disable comment, and generate both a RUF103 and RUF104 respectively.

So currently, the only way to cover a multiline function/class/loop header, without also covering the body, is to use end-of-line noqa suppressions on each line. By making #ruff:ignore a next-logical-line-only suppression, then it can cover those multiline headers without also covering the body.

Also, for the case of wanting to cover an entire class body, that's what the implicit block suppression was designed for:

class Foo:
	# ruff:disable[code]  # implicitly ends with the class body
	...

@ntBre
Copy link
Copy Markdown
Contributor

ntBre commented Feb 23, 2026

Fair enough

@amyreese amyreese force-pushed the amy/ruff-ignore-end-of-line branch from ecea25a to 6d8e7d1 Compare February 24, 2026 00:57
@ntBre ntBre requested a review from MichaReiser February 24, 2026 14:46
@amyreese amyreese force-pushed the amy/ruff-ignore-end-of-line branch from 6d8e7d1 to c1fba96 Compare February 27, 2026 00:58
@amyreese
Copy link
Copy Markdown
Contributor Author

Updated with preview gating to see what that would look like technically. The open question is if we want to warn when seeing suppression comments that aren't being utilized because ruff is running without preview mode, or if we should treat those as "invalid" expression comments. Treating them as invalid is perhaps the most "explicit" option, but might discourage experimentation with preview mode.

I personally don't foresee any reason to alter the intent of the suppression comments once we land them, but I'm not against the idea of preview mode either.

@amyreese amyreese changed the title Implement #ruff:ignore range suppressions Implement #ruff:ignore logical-line suppressions Feb 27, 2026
@dylwil3
Copy link
Copy Markdown
Collaborator

dylwil3 commented Feb 27, 2026

I like the idea of migrating away from noqa towards tool-specific directives. However, I'm not sure I understand well enough the motivation for the extended functionality of these suppression comments.

There was broad user support for block-level/range suppressions for the linter, but I'm having trouble finding issues where users are needing the kind of suppressions implemented here. Moreover, there have been reports of users getting a little confused about the number of different kinds of suppression comments we support (and their different semantics). So I would hesitate to add a brand new kind of suppression comment without a clear request from users.

It's true that there is an issue with the example

# ruff:disable[code]
def foo():
# ruff:enable[code]
	pass

when combined with the formatter, but I would view that as a bug or feature to request in the formatter - it's part of a broader issue where sometimes the formatter moves around directives, which I think it'd be good to do a better job at. Other edge cases might be addressed by changing the noqa-range for a rule. It's not clear to me that it's common enough of a situation where we need truly new functionality for a directive.

All that to say, and I'm happy to be overruled by other maintainers or the community: I think if we are going to support ruff: ignore[code] then it should be an exact synonym for noqa: code. If we wanted to extend the functionality then I could probably convinced that both should be extended to ignore the preceding logical line. But I think it's confusing to mix the behavior of ignoring both the next and preceding logical lines with the same syntax.

Brent argued in favor of Rust-style suppressions, which I think are neat but it's not obvious that they are a good fit for the Python ecosystem. First because I'm not sure if there is precedent, so users might be unfamiliar with that kind of things, and second because Python scoping is a lot different than Rust's. Also, now that you've implemented block suppressions with disable/enable there would be a lot of overlap in functionality (as you point out).

@amyreese
Copy link
Copy Markdown
Contributor Author

There were specific requests for a next-logical-line suppression format (I can try to dig it up) but it's also a very common pattern in other linters or type checkers (fixit, pyre, biome, eslint, etc). Biome and Fixit in particular also use ignore for both end-of-line and next-logical-line, and I think with examples, users find it quite intuitive to understand the affected region based on context.

Eg, since regular comments are almost exclusive used above the code they are documenting, an own-line suppression comment is most obviously going to cover the line below it, and by extension, increasing that scope to cover an entire logical line makes sense. Similarly, end-of-line suppression comments are clearly intended to cover the content they follow, so it makes sense to cover the physical line, and again covering an entire logical line when at the very end makes sense as an extension of that.

I think perhaps the least intuitive part is allowing a trailing comment on the first line to affect an entire multi-line statement, and I'm alright with dropping that piece — I don't remember off hand if/what part of noqa that was intended to replace/replicate. But end-of-line noqa can already affect an entire multi-line statement in ruff, eg, when placed at the end of a triple quoted string literal, so I think the symmetry of enabling that for all multi-line statements actually makes the system more intuitive for users, because it's less dependent on syntactic context.

@amyreese amyreese force-pushed the amy/ruff-ignore-end-of-line branch from c1fba96 to b6ffcf4 Compare March 2, 2026 21:00
@amyreese
Copy link
Copy Markdown
Contributor Author

amyreese commented Mar 2, 2026

Simplified the trailing comments to only cover an entire multi-line statement if the trailing comment is on the very last line. This still allows suppressing multiline string literals like with #noqa, and also reduces the amount of code/processing necessary to "look around" in both directions.

@amyreese amyreese force-pushed the amy/ruff-ignore-end-of-line branch from c9df742 to aef5860 Compare March 4, 2026 23:04
@MichaReiser
Copy link
Copy Markdown
Member

I do think a dedicated ruff: ignore comment will help establish Ruff's identity, and supporting own-line suppression should reduce issues with formatters. I also like that it can be used for both end-of-line and own line suppressions. I always struggled to remember ESLint's ignore syntax, and the fact that it's different for own-line and end-of-line suppressions didn't help.

I do agree with @dylwil3 that there's already a fair amount of confusion around the many suppression styles that Ruff supports, and adding yet another style without clear guidance and a migration strategy probably only makes this worse. This is why I feel we should wait and spend some more time to figure out some of the questions below before introducing ruff: ignore:

  • What's the recommended way to suppress diagnostics?
  • Will we continue to support both suppression styles? If so, for how long?
  • What changes are necessary to --add-noqa? Can users choose which style they prefer or will Ruff decide for them?
  • Is there an automated migration path?
  • Can users disable one or the other suppression style?
  • Should ruff: ignore respect the external linter codes?
  • Do we need to make any changes to the CLI interface?
  • I think we have to allow ruff: ignore without a code or it isn't a true replacement for noqa.

We might be able to skip some or all of those steps for now if we decide to only introduce own-line ruff: ignore comments, so that they act as an addition only and not as a replacement. But I'm not sure that's worth it. Ultimately, I think this comes down to how much time you want to spend on this feature now or if you want to focus on human readable names instead.

@amyreese
Copy link
Copy Markdown
Contributor Author

amyreese commented Mar 6, 2026

Some initial responses, though my position on them isn't super firm:

* What's the recommended way to suppress diagnostics?

I think we should immediately promote #ruff:ignore as the recommended suppression as soon as it's stabilized. Weak preference toward pushing users to use own-line variants?

* Will we continue to support both suppression styles? If so, for how long?

IMO we should document and consider noqa comments as backward-compatible "legacy" suppression format once we stabilize #ruff:ignore, and maintain support for it as "deprecated" in a future minor release, timeline maybe depending on how much maintenance burden we think it is and how it interacts with our push for human readable rule names.

* What changes are necessary to `--add-noqa`? Can users choose which style they prefer or will Ruff decide for them?

I'd probably suggest one of two options:

  • We add a separate --add-suppression flag that inserts the new #ruff:ignore comments instead, with a plan to deprecate and make --add-noqa a synonym in a future release
  • We change --add-noqa to use #ruff:ignore at the same time we stabilize the new suppressions and/or when we deprecate #noqa, and alias/rename the flag at the same time

I like the transition story of the first (giving users a temporary choice based on which flag they use) but I like the simplicity and reduction of code paths from the second option.

* Is there an automated migration path?

If we're going to deprecate #noqa, then I think we need to build something like a ruff upgrade <noqa|suppressions> command, and that could consider user config, so external codes stay on noqa, etc.

I would see this more as a requirement for deprecating #noqa than getting #ruff:ignore into preview, or even stabilization, as long as we have an appropriate plan/story for users.

* Can users disable one or the other suppression style?

I would suggest "no", and that --ignore-noqa would automatically ignore both styles.

* Should `ruff: ignore` respect the external linter codes?

We already do for ruff:disable range suppressions, so "yes"? But I'm not sure if it makes sense to since external linters presumably wouldn't be supporting #ruff:* comments.

* Do we need to make any changes to the CLI interface?

Other than renaming/aliasing flags that mention noqa, I wouldn't suggest any changes.

* I think we have to allow `ruff: ignore` without a code or it isn't a true replacement for `noqa`.

I don't think it needs to be a true replacement in that sense. We have the blanket-noqa rule, and we could in theory make a ruff upgrade migration automatically add appropriate codes during transition.

But I also think a #ruff:ignore[*] or similar syntax to convey that intent would be preferable over just #ruff:ignore or #noqa with no codes.

@ntBre
Copy link
Copy Markdown
Contributor

ntBre commented Mar 7, 2026

This is probably an entirely separate design discussion, but I'm still curious about the preference for a ruff upgrade command over a normal lint rule. I still find a lint rule more appealing, especially in this case, because it seems easy to habitually use a noqa comment and need another upgrade. So a normal lint rule with an autofix seems necessary anyway.

The rest of the proposal around preferring ruff:ignore over noqa makes sense to me.

@MichaReiser
Copy link
Copy Markdown
Member

I like the transition story of the first (giving users a temporary choice based on which flag they use) but I like the simplicity and reduction of code paths from the second option.

If we recommend ruff: ignore, than I think we at least need to support automatically adding ruff: ignore. That's a feature I would expect before stabilization as well as going through the CLI interface to make sure it uses up to date terminology.

If we're going to deprecate #noqa, then I think we need to build something like a ruff upgrade <noqa|suppressions> command, and that could consider user config, so external codes stay on noqa, etc.

I think we have to figure out a story for how to run Ruff alongside other linters before we can deprecate noqa. There are users that run multiple tools (e.g. #23780) and having to repeat the suppression comments seems annoying.

After thinking about this a little more. I actually don't think that noqa is as big a deal for ruff's identity. Yes, it originates from another linter, but the name itself is very generic, the same way as fmt: off, which we also have no intention of deprecating. It also raises the question what should happen with fmt: off, which is something we haven't discussed at all.

Overall, I think there are enough wrinkles and open design questions that I don't see any urgency in landing this PR and we may want to take a step back and discuss what's the main goal: Is it to allow own-line suppression? Does this require a new ruff: ignore suppression comment? Is it something else?

IMO, human-readable names seem more impactful right now, which is why I'd focus on that first. Unless we think that a ruff: ignore comment helps the human readable name transition (e.g. because it doesn't accept rule codes).

@amyreese
Copy link
Copy Markdown
Contributor Author

amyreese commented Mar 9, 2026

The main consideration to me is that as we transition users from rule codes to human readable names, especially in suppressions, then we lose cross-compatibility with other tools anyways. Would we want users mixing rule names into their #noqa comments alongside rule codes? I'm not sure how other tools would treat that, or whether that makes sense from a UX perspective. Is maintaining support for #noqa a long-term expectation?

@amyreese amyreese force-pushed the amy/ruff-ignore-end-of-line branch from aef5860 to d03c94e Compare March 12, 2026 21:23
@amyreese amyreese force-pushed the amy/ruff-ignore-end-of-line branch from 7089290 to 029f481 Compare April 20, 2026 14:59
@amyreese
Copy link
Copy Markdown
Contributor Author

Simplified the trailing comment handling so it always just covers a single physical line (but still works for multi-line string literals).

@amyreese amyreese merged commit 49aa2b2 into main Apr 20, 2026
44 checks passed
@amyreese amyreese deleted the amy/ruff-ignore-end-of-line branch April 20, 2026 18:38
amyreese added a commit that referenced this pull request Apr 24, 2026
Follow-up to #23404

Add support for `#ruff:file-ignore[code]` style file-level suppressions
as own-line comments at global module scope. The range covered by these
suppressions is always the entirety of the file:

```py
# ruff:file-ignore[ARG001]

def foo(
	arg1,
	arg2,
):
	pass

def bar(
	arg1,
	arg2,
):
	pass
```

This currently requires having preview mode enabled. Without preview
mode,
the comments are parsed and processed, but not materialized into active
suppressions at runtime.
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 suppression Related to supression of violations e.g. noqa

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants