-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Return DiagnosticGuard from Checker::report_diagnostic
#18232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Summary
--
This PR adds a `DiagnosticGuard` type to ruff that is adapted from the
`DiagnosticGuard` and `LintDiagnosticGuard` types from ty. This guard is
returned by `Checker::report_diagnostic` and derefs to a
`ruff_diagnostics::Diagnostic` (`OldDiagnostic`), allowing methods like
`OldDiagnostic::set_fix` to be called on the result. On `Drop` the
`DiagnosticGuard` pushes its contained `OldDiagnostic` to the `Checker`.
The main motivation for this is to make a following PR adding a `SourceFile` to
each diagnostic easier. For every rule where a `Checker` is available, this will
now only require modifying `Checker::report_diagnostic` rather than all the
rules.
In the few cases where we need to create a diagnostic before we know if we
actually want to emit it, there is a `DiagnosticGuard::defuse` method, which
consumes the guard without emitting the diagnostic. I was able to restructure
about half of the rules that naively called this to avoid calling it, but a
handful of rules still need it.
One of the fairly common patterns where `defuse` was needed initially was
something like
```rust
let diagnostic = Diagnostic::new(DiagnosticKind, range);
if !checker.enabled(diagnostic.rule()) {
return;
}
```
So I also added a `Checker::checked_report_diagnostic` method that handles this
check internally. That helped to avoid some additional `defuse` calls. The name
is a bit repetitive, so I'm definitely open to suggestions there. I included a
warning against using it in the docs since, as we've seen, the conversion from a
diagnostic to a rule is actually pretty expensive.
Test Plan
--
Existing tests
|
MichaReiser
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great. I didn't review all rule changes (there are too many).
I think it would be good to add rule to Violation to avoid the need to go through diagnostic.rule only to check if the rule is enabled (which could lead to a somewhat significant perf regression).
| range: TextRange, | ||
| ) -> Option<DiagnosticGuard<'chk, 'a>> { | ||
| let diagnostic = Diagnostic::new(kind, range); | ||
| if self.enabled(diagnostic.rule()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unfortunate that this has to go through diagnostic.rule which requires a lookup. I'm leaning towards adding a rule method to ViolationMetadata. That should be easy enough to derive and does make sense to me. This will also allow us to resolve the noqa code in a future version.
That makes me wonder if report_diagnostic should always to the self::enabled call but I suspect that handling with the Option return type is too annoying in many cases that it isn't worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh nice, I'll try to clean #18234 up for review at some point then. That makes sense to resolve the NoqaCode and store that on the Diagnostic instead but use the Rule directly here.
It does sound pretty appealing to always do the enabled check if it's cheap enough too. I think in most cases it should just be a let-else that returns if None, but it will be another big refactor to apply that everywhere for sure. I'm getting better at using ast-grep for these, though 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does sound pretty appealing to always do the enabled check if it's cheap enough too. I think in most cases it should just be a let-else that returns if None, but it will be another big refactor to apply that everywhere for sure. I'm getting better at using ast-grep for these, though 😄
Let's keep it at what we have for now. We can consider this if it proves necessary
| if is_guarded_by_try_except(expr, module, name, semantic) { | ||
| diagnostic.defuse(); | ||
| return; | ||
| } |
There was a problem hiding this comment.
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 avoid creating the diagnsotic by moving this check before the report_diagnostic call. Diagnostics are rather heavy weight and creating them for what's valid code is non ideal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reworked this a bit to avoid the defuse call. I also stored a QualifiedName directly on the rule struct to avoid a to_string call, but I needed to pass along a lifetime parameter in the derive macro using split_for_impl too. I could revert that part if we wanted, but it seemed like it could be helpful in general.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thought, I could definitely revert this. 2/4 airflow rules I updated actually needed Strings, so I couldn't change those to QualifiedName and the mix is a bit inconsistent.
crates/ruff_linter/src/rules/airflow/rules/suggested_to_update_3_0.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs
Outdated
Show resolved
Hide resolved
I avoided this initially, but it will be cheap enough once #18234 lands
* main: [ty] Support ephemeral uv virtual environments (#18335) Add a `ViolationMetadata::rule` method (#18234) Return `DiagnosticGuard` from `Checker::report_diagnostic` (#18232) [flake8_use_pathlib]: Replace os.symlink with Path.symlink_to (PTH211) (#18337) [ty] Support cancellation and retry in the server (#18273) [ty] Synthetic function-like callables (#18242) [ty] Support publishing diagnostics in the server (#18309) Add Autofix for ISC003 (#18256) [`pyupgrade`]: new rule UP050 (`useless-class-metaclass-type`) (#18334) [pycodestyle] Make `E712` suggestion not assume a context (#18328)
* main: (246 commits) [ty] Simplify signature types, use them in `CallableType` (astral-sh#18344) [ty] Support ephemeral uv virtual environments (astral-sh#18335) Add a `ViolationMetadata::rule` method (astral-sh#18234) Return `DiagnosticGuard` from `Checker::report_diagnostic` (astral-sh#18232) [flake8_use_pathlib]: Replace os.symlink with Path.symlink_to (PTH211) (astral-sh#18337) [ty] Support cancellation and retry in the server (astral-sh#18273) [ty] Synthetic function-like callables (astral-sh#18242) [ty] Support publishing diagnostics in the server (astral-sh#18309) Add Autofix for ISC003 (astral-sh#18256) [`pyupgrade`]: new rule UP050 (`useless-class-metaclass-type`) (astral-sh#18334) [pycodestyle] Make `E712` suggestion not assume a context (astral-sh#18328) put similar dunder-call tests next to each other (astral-sh#18343) [ty] Derive `PartialOrd, Ord` for `KnownInstanceType` (astral-sh#18340) [ty] Simplify `Type::try_bool()` (astral-sh#18342) [ty] Simplify `Type::normalized` slightly (astral-sh#18339) [ty] Move arviz off the list of selected primer projects (astral-sh#18336) [ty] Add --config-file CLI arg (astral-sh#18083) [ty] Tell the user why we inferred a certain Python version when reporting version-specific syntax errors (astral-sh#18295) [ty] Implement implicit inheritance from `Generic[]` for PEP-695 generic classes (astral-sh#18283) [ty] Add hint if async context manager is used in non-async with statement (astral-sh#18299) ...
Summary
This PR adds a
DiagnosticGuardtype to ruff that is adapted from theDiagnosticGuardandLintDiagnosticGuardtypes from ty. This guard is returned byChecker::report_diagnosticand derefs to aruff_diagnostics::Diagnostic(OldDiagnostic), allowing methods likeOldDiagnostic::set_fixto be called on the result. OnDroptheDiagnosticGuardpushes its containedOldDiagnosticto theChecker.The main motivation for this is to make a following PR adding a
SourceFileto each diagnostic easier. For every rule where aCheckeris available, this will now only require modifyingChecker::report_diagnosticrather than all the rules.In the few cases where we need to create a diagnostic before we know if we actually want to emit it, there is a
DiagnosticGuard::defusemethod, which consumes the guard without emitting the diagnostic. I was able to restructure about half of the rules that naively called this to avoid calling it, but a handful of rules still need it.One of the fairly common patterns where
defusewas needed initially was something likeSo I also added a
Checker::checked_report_diagnosticmethod that handles this check internally. That helped to avoid some additionaldefusecalls. The name is a bit repetitive, so I'm definitely open to suggestions there. I included a warning against using it in the docs since, as we've seen, the conversion from a diagnostic to a rule is actually pretty expensive.Test Plan
Existing tests