Skip to content

Emit a warning instead of an error for unknown rule selectors - #26113

Merged
ntBre merged 34 commits into
mainfrom
brent/defer-rule-parsing
Jun 24, 2026
Merged

Emit a warning instead of an error for unknown rule selectors#26113
ntBre merged 34 commits into
mainfrom
brent/defer-rule-parsing

Conversation

@ntBre

@ntBre ntBre commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR fixes #14443 by deferring rule parsing. This allows deserializing configuration files and
CLI arguments with unknown rule selectors and enables us to emit a warning instead of a hard error
when encountering an unknown selector. This will also help with #25887 because we can also defer
parsing until we have an accurate preview setting for checking whether human-readable names should
be allowed. I've left some of this infrastructure in place in this PR, with the preview argument
currently unused.

Test Plan

Existing tests updated to show warnings instead of errors

ntBre added 2 commits June 17, 2026 14:44
Summary
--

This PR fixes #14443 by deferring rule parsing. This allows deserializing configuration files and
CLI arguments with unknown rule selectors and enables us to emit a warning instead of a hard error
when encountering an unknown selector. This will also help with #25887 because we can also defer
parsing until we have an accurate `preview` setting for checking whether human-readable names should
be allowed. I've left some of this infrastructure in place in this PR, with the `preview` argument
currently unused.

Test Plan
--

Existing tests updated to show warnings instead of errors
@ntBre ntBre added the rule-selection Related to enabling or disabling rules label Jun 17, 2026
// More specified selectors take precedence over less specified selectors
.collect()
};
let mut safety_override_map: FxHashMap<Rule, (Specificity, Override)> =

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.

This is probably the least mechanical change in the PR. My initial naive approach was just calling selector.resolve in the existing Specificity::iter().flat_map but this would resolve/parse each selector once per Specificity (of which there are 8 variants), so this instead takes one loop over the selectors and stores their specificity in the map to avoid multiple resolutions.

}
if !unknown.is_empty() {
*contains_invalid_settings = true;
tracing::error!("Unknown rule selectors found in `{key}`: {unknown:?}");

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.

It's unfortunate to lose this more specific error message, but I think we'll recover it with something like ty's RangedValue in the future.

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.

Did you explore what it would take to preserve this information. Ideally, we'd keep source (which file, cli) + key

@astral-sh-bot

astral-sh-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

@ntBre
ntBre marked this pull request as ready for review June 17, 2026 19:40
@ntBre
ntBre requested a review from MichaReiser June 17, 2026 19:40

@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, thank you.

Unfortunately, the way this is implemented now is a breaking change. Users might rely on Ruff erroring for unknown rule codes. I think we have to preserve that behavior at least in non preview mode for now.

Did you explore trying to preserve the source? Not the exact location in the TOML, but at least enough information so that we can point the user to the right configuration file and maybe key.

Comment thread crates/ruff/tests/cli/lint.rs Outdated
Comment on lines +764 to +799
for args in [
["--select", "F481"],
["--extend-select", "F481"],
["--ignore", "F481"],
["--fixable", "F481"],
["--extend-fixable", "F481"],
["--unfixable", "F481"],
["--config", "lint.ignore=['F481']"],
["--config", "lint.extend-safe-fixes=['F481']"],
["--config", "lint.extend-unsafe-fixes=['F481']"],
["--per-file-ignores", "test.py:F481"],
["--extend-per-file-ignores", "test.py:F481"],
["--config", "lint.per-file-ignores={'test.py'=['F481']}"],
[
"--config",
"lint.extend-per-file-ignores={'test.py'=['F481']}",
],
] {
let output = fixture
.check_command()
.args(["--select", "F401"])
.args(args)
.arg("test.py")
.output()?;
assert_eq!(output.status.code(), Some(1), "arguments: {args:?}");
assert!(
str::from_utf8(&output.stdout)?.contains("F401"),
"arguments: {args:?}"
);
assert_eq!(
str::from_utf8(&output.stderr)?,
"warning: Invalid rule selector: `F481`\n",
"arguments: {args:?}"
);
}

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.

Nit: I'd write a test helper here that returns str::from_utf8(output.stderr) for a given arg and value instead of using a for loop in the test. For loops over test cases is very annoying in tests because it isn't obvious which specific case failed. The test helper avoids the main boilerplate, but you can still have a separate test function for each case and have the assert_eq in those test functions.

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.

Gotcha, that makes sense. I was initially put off by this too when Codex wrote it but figured it was okay in this instance. I can probably just turn them into test_cases instead of a loop, since we're currently asserting on the code, stdout, and stderr. Or we could just snapshot them I guess.

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 personally dislike test cases because I can’t run those tests from within my ide. But I wouldn’t object

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 will split them up. I need to customize the test names anyway, and I think I'd prefer just setting the function name over customizing that through test_case too.

}

#[test]
fn valid_toml_but_nonexistent_option_provided_via_config_argument() {

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.

We may need to change this test. I'm not sure it still captures what it originally intended.

Comment thread crates/ruff/tests/integration_test.rs Outdated
error: invalid value 'PREVIEW' for '--select <RULE_CODE>'

For more information, try '--help'.
warning: Invalid rule selector: `PREVIEW`

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.

Nit: Do we know whether the rule was removed? If so, could we emit a more specific error message?

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.

We know if Rules are removed from their group method, but I don't think we keep track of these removed selectors. I've added an error kind for removed selectors, but it currently just checks against the hard-coded list of PREVIEW and NURSERY.

Comment on lines +940 to +941
// Rules in preview are included here even if preview mode is disabled; it's safe to
// ignore disabled rules.

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.

Can you expand on what you mean by safe here? Do we not need to warn about unknown rules in per_file_ignores? What makes it safe?

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.

This was a pre-existing comment that was previously in PerFileIgnore::new. It means that it's "safe" to use all_rules here even though it doesn't filter out preview rules because preview rules are already ignored when preview itself is disabled.

Maybe the comment can be deleted anyway, but I added it when originally working on per-file-ignores I think.

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

impl UnresolvedRuleSelector {
pub fn resolve(&self, _preview: PreviewMode) -> Option<RuleSelector> {

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.

Why is _preview unused here? Remove?

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'm planning to use this immediately in #25887, so I wanted to make sure I had easy access to a PreviewMode wherever this was used. I can still remove it for now if you want.

Comment thread crates/ruff_linter/src/rule_selector.rs Outdated
pub fn resolve(&self, _preview: PreviewMode) -> Option<RuleSelector> {
RuleSelector::from_str(&self.selector)
.inspect_err(|_| {
warn_user_once_by_message!("Invalid rule selector: `{}`", self.selector);

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.

Maybe Unknown rule selector

It would also be nice if we could tell users where this rule selector was specified. Imagine a large mono repository with a 1000 configs. How are you supposed to find the rule selector (okay, you can do a grep, but this only works if all projects are using the same Ruff version)

}
if !unknown.is_empty() {
*contains_invalid_settings = true;
tracing::error!("Unknown rule selectors found in `{key}`: {unknown:?}");

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.

Did you explore what it would take to preserve this information. Ideally, we'd keep source (which file, cli) + key

Comment thread crates/ruff_linter/src/settings/fix_safety_table.rs Outdated
);

for (selector, o) in selectors {
let Some(selector) = selector.resolve(preview_options.mode) else {

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.

IMO, doing the logging in resolve feels a bit implicit. Maybe resolve_and_warn or move the error handling out of resolve (I don't know how annoying that would be).

Or, what we do in ty. We have a custom error, and the error has helper methods. E.g. this could be

let selector = match selector.resolve(preview_options.mode) {
	Ok(selector) => selector,
	Err(err) => {
		err.log_warning("unsafe_fixes");
		continue;
	}
};

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.

Ah yeah, this doesn't seem as bad as I thought. This is only called in 3 places, so I'll just move the logging out of resolve.

Comment thread crates/ruff_linter/src/settings/fix_safety_table.rs Outdated
@ntBre

ntBre commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Did you explore trying to preserve the source? Not the exact location in the TOML, but at least enough information so that we can point the user to the right configuration file and maybe key.

Not really beyond reading how ty does it. I tried to structure things so that this would be easy in the future but thought we might leave that for a follow-up. Maybe it makes more sense to do it here, though, in line with a couple of your inline comments.

@ntBre
ntBre marked this pull request as draft June 18, 2026 13:46
@ntBre ntBre added the preview Related to preview mode features label Jun 18, 2026
@ntBre

ntBre commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

I adapted a lot of ty's RangedValue source information and addressed most of the other comments, but I'm still working through some of Codex's review findings. Namely, it's now possible to panic here in the editor if invalid settings are present:

Self::with_editor_settings(editor_settings, root, configuration).expect(
"editor configuration should merge successfully with default configuration",
)

because the resolution is actually deferred too long for the current structure. It's looking kind of involved to work around this, so I still need to think about it a bit more.

I think the new snapshot file names are also invalid on Windows, but that's obviously easier to fix.

@MichaReiser

Copy link
Copy Markdown
Member

This sounds very much like #25024. I think the fix there is easy, see my comment.

@ntBre
ntBre requested review from a team as code owners June 24, 2026 15:48
@astral-sh-bot

astral-sh-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 94.47%. The percentage of expected errors that received a diagnostic held steady at 89.10%. The number of fully passing files held steady at 95/134.

@astral-sh-bot

astral-sh-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

Memory usage report

Memory usage unchanged ✅

@astral-sh-bot

astral-sh-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

Lint rule Added Removed Changed
unresolved-reference 0 13 0
invalid-return-type 3 0 0
invalid-argument-type 1 0 0
unresolved-attribute 1 0 0
Total 5 13 0
Raw diff (18 changes)
hydpy (https://github.com/hydpy-dev/hydpy)
- hydpy/core/parametertools.py:3050:23 error[unresolved-reference] Name `__class__` used when not defined
- hydpy/core/sequencetools.py:2123:23 error[unresolved-reference] Name `__class__` used when not defined
- hydpy/core/sequencetools.py:3392:23 error[unresolved-reference] Name `__class__` used when not defined
- hydpy/core/sequencetools.py:3663:23 error[unresolved-reference] Name `__class__` used when not defined
- hydpy/core/sequencetools.py:3802:22 error[unresolved-reference] Name `__class__` used when not defined
- hydpy/core/sequencetools.py:4016:27 error[unresolved-reference] Name `__class__` used when not defined
- hydpy/core/variabletools.py:2292:27 error[unresolved-reference] Name `__class__` used when not defined
+ hydpy/core/variabletools.py:2293:13 error[unresolved-attribute] Object of type `<super: <class 'MixinFixedShape'>, type[Self@shape]>` has no attribute `shape`
- hydpy/models/evap/evap_logs.py:77:22 error[unresolved-reference] Name `__class__` used when not defined
+ hydpy/models/evap/evap_logs.py:78:25 error[invalid-argument-type] Argument to function `Variable.shape` is incorrect: Expected `int | tuple[int, ...]`, found `tuple[Literal[1], int | tuple[int, ...]]`

pywin32 (https://github.com/mhammond/pywin32)
- pythonwin/pywin/test/test_pywin.py:141:31 error[unresolved-reference] Name `__class__` used when not defined
- pythonwin/pywin/test/test_pywin.py:141:55 error[unresolved-reference] Name `__class__` used when not defined
- pythonwin/pywin/test/test_pywin.py:287:25 error[unresolved-reference] Name `__class__` used when not defined

rotki (https://github.com/rotki/rotki)
+ rotkehlchen/tasks/historical_balances.py:186:20 error[invalid-return-type] Return type does not match returned value: expected `list[tuple[Bucket, Literal[EventDirection.IN, EventDirection.OUT]]]`, found `list[tuple[Bucket, Literal[EventDirection.IN, EventDirection.OUT]] | tuple[Self@from_event, EventDirection]]`
+ rotkehlchen/tasks/historical_balances.py:195:16 error[invalid-return-type] Return type does not match returned value: expected `list[tuple[Bucket, Literal[EventDirection.IN, EventDirection.OUT]]]`, found `list[tuple[Bucket, Literal[EventDirection.IN, EventDirection.OUT]] | tuple[Self@from_event, EventDirection]]`

scrapy (https://github.com/scrapy/scrapy)
- tests/test_item.py:293:28 error[unresolved-reference] Name `__class__` used when not defined

steam.py (https://github.com/Gobot1234/steam.py)
+ steam/ext/commands/cooldown.py:52:43 error[invalid-return-type] Function can implicitly return `None`, which is not assignable to return type `BucketTypeType`

sympy (https://github.com/sympy/sympy)
- sympy/core/cache.py:209:24 error[unresolved-reference] Name `__class__` used when not defined

Full report with detailed diff (timing results)

@ntBre
ntBre removed request for a team June 24, 2026 16:03
@ntBre
ntBre merged commit 62859df into main Jun 24, 2026
63 checks passed
@ntBre
ntBre deleted the brent/defer-rule-parsing branch June 24, 2026 16:13
charliermarsh added a commit that referenced this pull request Jun 24, 2026
## Summary

Record `ruff_ranged_value` as configured for crates.io trusted
publishing after #26113 made it publishable.

The bootstrap script uses `.known-crates` as a persistent checkpoint and
skips entries on later runs. Checking in the generated result makes
subsequent bootstrap runs a no-op until another workspace package
becomes publishable.

A dry run confirms that all 37 publishable workspace packages are
recorded and no setup work remains.
ntBre added a commit that referenced this pull request Jun 25, 2026
## Summary

This PR enables using rule names as selectors in preview by also
attempting to parse a selector as a human-readable name in
`UnresolvedRuleSelector::resolve`. If this succeeds and preview is
enabled, the rule is activated, and if preview is disabled you get a
custom error message.

## Test Plan

A few new CLI tests. I figured the tests from
#26113 cover the shared behavior
for the other selectors well enough and just focused on `select` as an
example for both the CLI and config file.
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-selection Related to enabling or disabling rules

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Make *any* unknown rule code in "ignore" setting a warning, not an error

2 participants