Emit a warning instead of an error for unknown rule selectors - #26113
Conversation
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
| // More specified selectors take precedence over less specified selectors | ||
| .collect() | ||
| }; | ||
| let mut safety_override_map: FxHashMap<Rule, (Specificity, Override)> = |
There was a problem hiding this comment.
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:?}"); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Did you explore what it would take to preserve this information. Ideally, we'd keep source (which file, cli) + key
|
MichaReiser
left a comment
There was a problem hiding this comment.
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.
| 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:?}" | ||
| ); | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I personally dislike test cases because I can’t run those tests from within my ide. But I wouldn’t object
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
We may need to change this test. I'm not sure it still captures what it originally intended.
| error: invalid value 'PREVIEW' for '--select <RULE_CODE>' | ||
|
|
||
| For more information, try '--help'. | ||
| warning: Invalid rule selector: `PREVIEW` |
There was a problem hiding this comment.
Nit: Do we know whether the rule was removed? If so, could we emit a more specific error message?
There was a problem hiding this comment.
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.
| // Rules in preview are included here even if preview mode is disabled; it's safe to | ||
| // ignore disabled rules. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| impl UnresolvedRuleSelector { | ||
| pub fn resolve(&self, _preview: PreviewMode) -> Option<RuleSelector> { |
There was a problem hiding this comment.
Why is _preview unused here? Remove?
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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:?}"); |
There was a problem hiding this comment.
Did you explore what it would take to preserve this information. Ideally, we'd keep source (which file, cli) + key
| ); | ||
|
|
||
| for (selector, o) in selectors { | ||
| let Some(selector) = selector.resolve(preview_options.mode) else { |
There was a problem hiding this comment.
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;
}
};There was a problem hiding this comment.
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.
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. |
|
I adapted a lot of ty's ruff/crates/ruff_server/src/session/index/ruff_settings.rs Lines 117 to 119 in a26853c 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. |
|
This sounds very much like #25024. I think the fix there is easy, see my comment. |
Typing conformance resultsNo changes detected ✅Current numbersThe 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. |
Memory usage reportMemory usage unchanged ✅ |
|
| 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## 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.
## 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.
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
previewsetting for checking whether human-readable names shouldbe allowed. I've left some of this infrastructure in place in this PR, with the
previewargumentcurrently unused.
Test Plan
Existing tests updated to show warnings instead of errors