Skip to content

[ruff] Add rule-codes-in-selectors (RUF201) - #26772

Merged
ntBre merged 18 commits into
mainfrom
brent/lint-selectors
Jul 16, 2026
Merged

[ruff] Add rule-codes-in-selectors (RUF201)#26772
ntBre merged 18 commits into
mainfrom
brent/lint-selectors

Conversation

@ntBre

@ntBre ntBre commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds a new lint rule for TOML config files that checks for the use of rule codes as
selectors and offers a fix to replace them with human-readable names. At a high-level, the
implementation just deserializes a TOML config into a stripped down version of our normal
configuration types and then checks each of its Spanned selector entries. Unless I oversimplified
this too much, it didn't end up being as tricky as I thought. The main tricky part was adding basic
support for fixes in TOML files, but even this was pretty straightforward.

Codex was concerned about calling this a safe fix because applying the fix with --preview, or
another temporary way of enabling preview, would break the config if preview isn't always enabled.
That seemed to go beyond our usual fix safety policy to me, but I'm also happy to mark the fix as
unsafe if that's preferable.

Test Plan

New tests covering all of the expected selectors and new CLI tests to make sure that ruff.toml
files get linted and respect various ways of suppressing fixes. Previously we only linted
pyproject.toml files for RUF200 and bypassed some of the checks in LintContext. I left the
change to use LintContext as a separate commit since it also affects RUF200 and I briefly
considered splitting it off into a separate PR.

@ntBre ntBre added rule Implementing or modifying a lint rule preview Related to preview mode features labels Jul 13, 2026
ntBre added 2 commits July 13, 2026 14:10
Summary
--

This PR adds a new lint rule for TOML config files that checks for the use of rule codes as
selectors and offers a fix to replace them with human-readable names. At a high-level, the
implementation just deserializes a TOML config into a stripped down version of our normal
configuration types and then checks each of its `Spanned` selector entries. Unless I oversimplified
this too much, it didn't end up being as tricky as I thought. The main tricky part was adding basic
support for fixes in TOML files, but even this was pretty straightforward.

Codex was concerned about calling this a safe fix because applying the fix with `--preview`, or
another temporary way of enabling preview, would break the config if preview isn't always enabled.
That seemed to go beyond our usual fix safety policy to me, but I'm also happy to mark the fix as
unsafe if that's preferable.

Test Plan
--

New tests covering all of the expected selectors and new CLI tests to make sure that `ruff.toml`
files get linted and respect various ways of suppressing fixes. Previously we only linted
`pyproject.toml` files for `RUF200` and bypassed some of the checks in `LintContext`. I left the
change to use `LintContext` as a separate commit since it also affects `RUF200` and I briefly
considered splitting it off into a separate PR.
@ntBre
ntBre force-pushed the brent/lint-selectors branch from 6ab1f76 to 6a2d6a5 Compare July 13, 2026 18:10
@astral-sh-bot

astral-sh-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

Memory usage report

Memory usage unchanged ✅

@astral-sh-bot

astral-sh-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

No diagnostic changes detected ✅

Full report with detailed diff (timing results)

@astral-sh-bot

astral-sh-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+1861 -0 violations, +0 -0 fixes in 27 projects; 29 projects unchanged)

DisnakeDev/disnake (+47 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ pyproject.toml:174:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:175:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:176:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:177:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:178:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:181:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:183:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:184:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:185:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:187:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
... 37 additional changes omitted for project

RasaHQ/rasa (+9 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ pyproject.toml:349:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:350:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:351:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:352:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:353:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:354:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:355:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:371:6: rule-codes-in-selectors [*] Rule code used instead of name in `per-file-ignores`
+ pyproject.toml:374:6: rule-codes-in-selectors [*] Rule code used instead of name in `per-file-ignores`

apache/airflow (+244 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select ALL

+ airflow-ctl/pyproject.toml:101:19: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ airflow-ctl/pyproject.toml:101:29: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ airflow-ctl/pyproject.toml:101:37: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ airflow-ctl/pyproject.toml:107:34: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ airflow-ctl/pyproject.toml:110:50: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ airflow-ctl/pyproject.toml:110:59: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ airflow-ctl/pyproject.toml:110:67: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ airflow-ctl/pyproject.toml:111:55: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ airflow-ctl/pyproject.toml:111:64: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ airflow-ctl/pyproject.toml:111:72: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
... 234 additional changes omitted for project

apache/superset (+22 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select ALL

+ pyproject.toml:414:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.select`
+ pyproject.toml:418:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.select`
+ pyproject.toml:419:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.select`
+ pyproject.toml:436:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:437:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:438:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:439:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:440:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:441:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:454:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
... 12 additional changes omitted for project

bokeh/bokeh (+24 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select ALL

+ pyproject.toml:247:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:248:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:249:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:250:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:251:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:252:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:253:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:254:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:255:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
+ pyproject.toml:256:6: rule-codes-in-selectors [*] Rule code used instead of name in `ignore`
... 14 additional changes omitted for project

ibis-project/ibis (+60 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ pyproject.toml:563:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:564:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:565:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:566:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:567:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:568:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:569:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:570:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:571:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:572:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
... 50 additional changes omitted for project

langchain-ai/langchain (+362 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ libs/core/pyproject.toml:102:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ libs/core/pyproject.toml:104:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ libs/core/pyproject.toml:105:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ libs/core/pyproject.toml:107:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ libs/core/pyproject.toml:108:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ libs/core/pyproject.toml:111:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ libs/core/pyproject.toml:116:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.unfixable`
+ libs/core/pyproject.toml:133:41: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ libs/core/pyproject.toml:134:35: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ libs/core/pyproject.toml:136:23: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
... 352 additional changes omitted for project

latchbio/latch (+57 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ pyproject.toml:100:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:101:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:102:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:103:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:104:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:105:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:106:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:107:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:108:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:109:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
... 47 additional changes omitted for project

lnbits/lnbits (+16 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ pyproject.toml:205:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:206:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.ignore`
+ pyproject.toml:222:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ pyproject.toml:225:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ pyproject.toml:226:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ pyproject.toml:227:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ pyproject.toml:230:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ pyproject.toml:233:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ pyproject.toml:236:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
+ pyproject.toml:237:6: rule-codes-in-selectors [*] Rule code used instead of name in `lint.per-file-ignores`
... 6 additional changes omitted for project

... Truncated remaining completed project reports due to GitHub comment length restrictions

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
rule-codes-in-selectors 1861 1861 0 0 0

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

@ntBre

ntBre commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

I was highly alarmed by the ecosystem results, which show completely wrong line and column numbers, but we rewrite config files as part of our ecosystem analysis to change any necessary settings:

and tomli/tomli_w don't support preserving comments: https://github.com/hukkin/tomli#is-comment-preserving-round-trip-parsing-supported. I was hoping they might at least preserve some kind of Span so that we could manually splice out the problematic settings, but that doesn't seem to be the case either.

A CLI override would be a better way around this (#10345), but after #22410, it seems that --config required-version=... doesn't work anymore either:

$ printf 'required-version = ">999"\n' > ruff.toml
$ touch test.py
$ ruff check --config 'required-version = ">=0"' .
ruff failed
  Cause: Failed to load configuration `/private/var/folders/h6/f4fnzd4j1j5062v0hf073bm40000gn/T/tmp.htAWvk7AUu/ruff.toml`
  Cause: Required version `>999` does not match the running version `0.15.15`
$ uvx ruff@0.14 check --config 'required-version = ">=0"' .
All checks passed!

We should probably fix these, especially if we add more TOML rules, but it seems beyond the scope of this PR.

The other ecosystem results I've looked at otherwise appear to be correct.

@ntBre
ntBre marked this pull request as ready for review July 13, 2026 20:46
@ntBre
ntBre requested a review from MichaReiser July 13, 2026 20:46

@MichaReiser MichaReiser left a comment

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.

This cool.

I think this PR would have been slightly easier to review if split into smaller PRs. But I can see how this is awkward in some sense, because you can only start adding tests in later PRs. But roughly, the work here is

  • Add support for linting ruff.toml, including ruff-test
  • Add the new rule (maybe this could be one PR with add linting support)
  • Add support for fixing in toml files

I leave it up to you if you want to split the PR or leave it as one (I have reviewed most of it by now)

I do think we should make a few changes to this PR:

  • Migrate testing to ruff-test
  • Move parsing out of the lint rule into lint_toml. Each rule parsing the configuration on its own doesn't scale
  • If we can, avoid duplicating the Configuration struct. This may require adding Span's to `Configuration. If so, it would probably be good to do this as a separate PR
  • Maybe, only change the default include under preview.

I also suggest testing whether the new lint and fixes work in Ruff's LSP. We should make a decision if this is something we want to fix, but I think it's fine to do this as a separate PR.

Comment thread crates/ruff_linter/src/pyproject_toml.rs Outdated
}

/// Generate [`Diagnostic`]s for a TOML configuration file, iteratively fixing until stable.
pub fn lint_fix_toml<'a>(

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.

It's a bit unfortunate that we need separate lint_toml and lint_fix_toml functions. It would be nice if they could share more, but I think that's only really possible by introducing more abstraction or a more lazy model, like Salsa's.

Given that this PR is already large, I think the current approach is fine

Comment thread crates/ruff_linter/src/pyproject_toml.rs Outdated
Comment on lines +65 to +68
let Ok(Some(config_file)) = ConfigFile::from_toml_str(source_file.source_text(), source_type)
else {
return;
};

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 don't think each rule parsing the toml file itself scales well. We should move this out into lint_toml and pass the parsed configuration instead of the raw source type.

/// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
#[option(
default = r#"["*.py", "*.pyi", "*.pyw", "*.ipynb", "*.md", "**/pyproject.toml"]"#,
default = r#"["*.py", "*.pyi", "*.pyw", "*.ipynb", "*.md", "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml"]"#,

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.

Do we need to gate this behind preview? Or is the rule itself gated behind preview? Do we need to mention that this only applies in preview?

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.

Yes, the rule itself is in preview. I saw *.md here, which is also still in preview, and thought that I should include the other new formats too.

}
}

fn toml_string_content_range(source: &str, range: Range<usize>) -> Option<Range<usize>> {

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.

Should this return a TextRange instead (and take a TextRange as input?)

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 inlined this into a RuleCode::from_spanned helper that ties the rule code and range together. I think that makes the relationship to the Rule::from_code call clearer too #26772 (comment).

Comment on lines +194 to +195
// Keep these fields in sync with the selector-valued fields in
// `ruff_workspace::options::LintCommonOptions`.

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.

The comment here is not relevant. Same in the Ruff section. But I am concerned that we'll fail to keep this struct up to date with our main Configuration.

Comment thread crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_selectors.rs Outdated
let resolved_file = resolved_file.context("Iterating the files in the repository failed")?;
// For some reason it does not filter in the beginning
if resolved_file.file_name() == "pyproject.toml" {
if ["pyproject.toml", "ruff.toml", ".ruff.toml"]

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.

What about ruff format?

Codex reports

[P2] Adding Ruff configs to stable discovery breaks formatter range mode. settings.rs:146
With one Python file and ruff.toml, ruff format --check --range 1:1-1:9 DIR now fails because the formatter validates the discovered-file count before filtering TOML. Previously the directory resolved to one formattable file. Filter TOML before the range check or avoid adding these files to stable formatter discovery.

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.

Interesting, none of my Codex reviews ever caught this. Possibly because this reproduces on main with pyproject.toml:

$ touch pyproject.toml
$ touch try.py
$ uvx ruff@latest format --range 1:1-1:9 .
ruff failed
  Cause: The `--range` option is only supported when formatting a single file but the specified paths resolve to 2 files.

Is passing a directory to --range meant to be a supported operation?

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.

not really... I should have noticed this

Comment on lines +407 to +408
let (diagnostics, fixed) =
if matches!(fix_mode, flags::FixMode::Apply | flags::FixMode::Diff) {

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.

It would be nice if we could reuse more with the python code path below. The structure here now feels very similar. I don't think we should push it very hard, but if you see an opportunity to do so

@ntBre
ntBre marked this pull request as draft July 14, 2026 15:04
ntBre added a commit that referenced this pull request Jul 14, 2026
Summary
--

This PR was split off from #26772 to add support for testing TOML lint
rules to mdtest. We use the
toml_parser crate to extract comments and now call `lint_pyproject_toml`
in ruff_mdtest. Because we already use `toml` blocks for test
configuration and because Ruff's current TOML lints need to know the
filename, I opted to require a filename for linted TOML blocks. For
example,

````markdown
```toml
lint.select = ["F401"]
```
````

remains a configuration block, but

````markdown
`ruff.toml`:

```toml
lint.select = ["F401"]
```
````

will be linted.

Test Plan
--

Added one test for RUF200 and unit tests for the parsing itself.
ntBre added a commit that referenced this pull request Jul 15, 2026
Summary
--

This PR was spun off from #26772 to handle some of the refactors to
RUF200. The first non-empty
commit is purely moving the implementation of RUF200 from the general
`lint_pyproject_toml` function
into its own rule file. In #26772, `lint_pyproject_toml` will become
`lint_toml` and invoke multiple
rules.

The second commit drops the overly defensive range check, as noted in
#26772 (comment).

The third commit refactors the rule to use a `LintContext`. This not
only sets up to run multiple
rules in this function, but also fixes a latent bug in rule suppression
for RUF200. Because the rule
previously checked whether it was enabled via the
`LinterSettings::rules.enabled` directly, it
didn't take into account settings like `per-file-ignores` (this also
affects `unfixable` and other
fix-related settings for RUF201). The fourth commit contains a new
mdtest for this that currently
fails on main.

Finally, the fifth commit moves parsing out of RUF200. We still parse
into a `PyProjectToml` for
now, but at least this sets us up to share parsing between rules once we
find the right shared
representation.

Test Plan
--

New mdtest mentioned above and then rebasing #26772 onto this.
@ntBre

ntBre commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review! I think I've addressed the feedback. In particular, I split off PRs for adding TOML support to mdtests and refactoring RUF200 to use a LintContext, then I:

  • converted the majority of tests here to mdtests (one remaining CLI test for ruff.toml discovery)
  • switched to a DeTable approach instead of duplicating the config schema, which also allows sharing part of the parse with RUF200 and avoids the chained iterator issue
  • restored the stable includes to omit ruff.toml and .ruff.toml

I also looked into sharing more of the FixMode code, but unfortunately I think the two sections are different enough that it's a bit awkward to share.

Similarly, the lint and fixes do not currently work in the LSP, as I think you suspected. I can follow up on that since this PR is already quite large (although much smaller now!).

@ntBre
ntBre marked this pull request as ready for review July 15, 2026 17:53
@MichaReiser
MichaReiser self-requested a review July 16, 2026 06:12

@MichaReiser MichaReiser left a comment

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.

Nice solution by using Table directly.


```toml
[lint]
select = ["'F401'"]

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.

Should we extend this example with a non-invalid-quotes examples?

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.

Added a valid rule snapshot to the same example.

2 | lint.select = ["F401"]
| ^^^^
|
help: Replace rule code with name

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'm not convinced we should do this, but raising in case you have an opinion/idea on how to improve this.

Should we mention the rule name here? It feels a bit redundant if the fix is enabled too, but seems useful if the fix is missing.

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.

Sure, I like that idea.

let Some(values) = value.get_ref().as_array() else {
continue;
};
check_selector_array(context, values, selector, in_lint_table);

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.

Hmm, I think we should split SELECTORS into two lists. One for arrays, and another for tables. Or make SELECTORS store paths, e.g. ["per-file-ignores", "file"] to make the path unambigous. Right now, the rule would rewrite a select = { blabla = [] } (blabla also works for per-file-ignores.). So I do think we need to make sure that we only rewrite allowlisted arrays.

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.

Interesting. Codex mentioned something similar, but I dismissed it because select = { blabla = [] } is an invalid config and should be rejected by normal config loading before we run any rules. But it's probably safer to specify which settings are expected to be tables anyway.

}
}

const SELECTORS: &[&str] = &[

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.

Should we add some documentation to RuleSelector or elsewhere to hint towards updating this rule?

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.

Added to UnresolvedRuleSelector since hopefully that's the type someone would reach for when adding new selectors.

@ntBre
ntBre merged commit f3cf7c8 into main Jul 16, 2026
62 checks passed
@ntBre
ntBre deleted the brent/lint-selectors branch July 16, 2026 14:05
jtraglia added a commit to jtraglia/ruff-md that referenced this pull request Jul 26, 2026
Rebasing the fork's commits onto each new upstream release could not work.
A 3-way merge conflicts even when both sides only *add* code near each
other, so upstream inserting a function next to `extract_python` was
enough to stop it, and every release bumps `version` in pyproject.toml
directly below the fork's `name` edit. The job had been failing nightly
since 0.15.15, leaving the fork nine releases behind.

Each release is now rebuilt from a clean checkout of the upstream tag,
producing exactly one commit on top of it. fork/apply.sh does four dumb
things, in increasing order of how much each can go wrong:

  1. copy in workflows that exist only in this fork
  2. rewrite two known-constant lines (pyproject name, PACKAGE_NAME)
  3. rewrite release.yml and check.rs structurally
  4. apply fork/fork.patch with `patch --fuzz=3`

Nothing merges anywhere. Steps 1-3 either succeed or abort with a specific
message. Only step 4 can reject a hunk, and a rejection does not abort,
because a hunk also rejects when upstream has implemented that change
itself: 0.16.0 added `*.md` to the default include list and removed the
markdown preview gate, making two of the fork's patches obsolete.

Whether the result is actually good is decided by building it and checking
that a `.md` file still lints, which gates tagging and publishing. That
matters more than it sounds — a tree with the check.rs hunk rejected still
compiles, it just silently stops linting markdown.

Anything anchored to something upstream keeps extending is a structural
rewrite rather than a patch hunk:

  - release.yml: depot runners, plus `if: false` on every job outside a
    six-job allowlist. An allowlist, so a job upstream adds later is off
    by default. 0.15.19 added `custom-publish-crates`, which would
    otherwise have tried to publish to crates.io from this fork.
  - check.rs: appends to the `matches!(SourceType::from(path), ..)` arm.
    Upstream extends that list whenever ruff learns a new file type
    (astral-sh#26772 added `TomlSourceType::Ruff`), and this was the only hunk in
    fork.patch that ever broke.

Verified by replaying all nine releases from 0.15.15 to 0.16.0: zero
rejected hunks in every one, against 7/9 for the same patch before
check.rs was moved out of it. On 0.16.0 the result builds, passes
`cargo test -p ruff_markdown`, is rustfmt-clean, and reports F401 on the
correct line inside a fenced code block.

fork/fork.patch is the source of truth for the fork's source changes
rather than the branch history, since the branch is force-pushed to each
new release. fork/regen.sh rewrites it from a working tree. Each release
commit carries fork/ and auto-release.yml forward, so it contains the
machinery that produces the next one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ntBre added a commit that referenced this pull request Jul 28, 2026
Summary
--

Rough draft of RUF200/RUF201 support in the LSP, currently stacked on #26772.

Test Plan
--

Codex added some e2e tests, which I need to review, but I also want to test manually in VS Code.
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.

2 participants