Skip to content

Conversation

ntBre
Copy link
Contributor

@ntBre ntBre commented May 20, 2025

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

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

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
@ntBre ntBre added internal An internal refactor or improvement diagnostics Related to reporting of diagnostics. labels May 20, 2025
Copy link
Contributor

github-actions bot commented May 20, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@ntBre ntBre marked this pull request as ready for review May 20, 2025 20:47
@ntBre ntBre requested a review from AlexWaygood as a code owner May 20, 2025 20:47
@ntBre ntBre requested review from MichaReiser and removed request for AlexWaygood May 20, 2025 20:47
Copy link
Member

@MichaReiser MichaReiser left a 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()) {
Copy link
Member

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.

Copy link
Contributor Author

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 😄

Copy link
Member

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

Comment on lines 1214 to 1217
if is_guarded_by_try_except(expr, module, name, semantic) {
diagnostic.defuse();
return;
}
Copy link
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 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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@ntBre ntBre merged commit a3ee6bb into main May 28, 2025
34 checks passed
@ntBre ntBre deleted the brent/report-violation-builder branch May 28, 2025 11:41
dcreager added a commit that referenced this pull request May 28, 2025
* 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)
carljm added a commit to MatthewMckee4/ruff that referenced this pull request May 28, 2025
* 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)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
diagnostics Related to reporting of diagnostics. internal An internal refactor or improvement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants