Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions crates/ruff_db/src/diagnostic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,11 @@ impl Diagnostic {
.find(|ann| ann.is_primary)
}

/// Returns a mutable borrow of all annotations of this diagnostic.
pub fn annotations_mut(&mut self) -> impl Iterator<Item = &mut Annotation> {
Arc::make_mut(&mut self.inner).annotations.iter_mut()
}

/// Returns the "primary" span of this diagnostic if one exists.
///
/// When there are multiple primary spans, then the first one that was
Expand Down Expand Up @@ -310,6 +315,11 @@ impl Diagnostic {
&self.inner.subs
}

/// Returns a mutable borrow of the sub-diagnostics of this diagnostic.
pub fn sub_diagnostics_mut(&mut self) -> impl Iterator<Item = &mut SubDiagnostic> {
Arc::make_mut(&mut self.inner).subs.iter_mut()
}

/// Returns the fix for this diagnostic if it exists.
pub fn fix(&self) -> Option<&Fix> {
self.inner.fix.as_ref()
Expand Down Expand Up @@ -487,6 +497,16 @@ impl Diagnostic {
other.expect_range().start(),
))
}

/// Sort this diagnostic's sub-diagnostics by descending severity.
///
/// This is particularly helpful to move `help`-level sub-diagnostics to the end, after any
/// longer, `info`-level diagnostics.
pub fn sort_sub_diagnostics(&mut self) {
Arc::make_mut(&mut self.inner)
.subs
.sort_by_key(|sub| std::cmp::Reverse(sub.inner.severity));
}
}

#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
Expand Down Expand Up @@ -621,6 +641,11 @@ impl SubDiagnostic {
&self.inner.annotations
}

/// Returns a mutable borrow of the annotations of this sub-diagnostic.
pub fn annotations_mut(&mut self) -> impl Iterator<Item = &mut Annotation> {
self.inner.annotations.iter_mut()
}

/// Returns a shared borrow of the "primary" annotation of this diagnostic
/// if one exists.
///
Expand Down
7 changes: 6 additions & 1 deletion crates/ruff_db/src/diagnostic/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,12 @@ impl<'a> ResolvedDiagnostic<'a> {
.annotations
.iter()
.filter_map(|ann| {
let path = ann.span.file.path(resolver);
let path = ann
.span
.file
.relative_path(resolver)
.to_str()
.unwrap_or_else(|| ann.span.file.path(resolver));
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.

Out of curiosity, what is the motivation for this change?

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.

Just to match the path in from_diagnostic above. This isn't used in this PR now that we're using a secondary annotation, so I can revert it!

I'll double-check if it's actually needed for full diagnostics too.

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.

Using path instead of relative_path changes some of our CLI snapshots for the main diagnostic, so I think we'll need this for sub-diagnostics too, once we use any sub-diagnostics with paths in Ruff.

    ────────────────────────────────────────────────────────────────────────────────
    -old snapshot
    +new results
    ────────────┬───────────────────────────────────────────────────────────────────
        0     0 │ success: false
        1     1 │ exit_code: 1
        2     2 │ ----- stdout -----
        3     3 │ F401 [*] `bar` imported but unused
        4       │- --> bar.py:2:8
              4 │+ --> /tmp/.tmpJqw1Qi/bar.py:2:8
        5     5 │   |
        6     6 │ 2 | import bar   # unused import
        7     7 │   |        ^^^
        8     8 │   |
        9     9 │ help: Remove unused import: `bar`
       10    10 │ 
       11    11 │ F401 [*] `foo` imported but unused
       12       │- --> foo.py:2:8
             12 │+ --> /tmp/.tmpJqw1Qi/foo.py:2:8
       13    13 │   |
       14    14 │ 2 | import foo   # unused import
       15    15 │   |        ^^^
       16    16 │   |
    ────────────┴───────────────────────────────────────────────────────────────────

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 think I'll go ahead and leave this instead of reverting, since it seems like it will be useful in the future)

let diagnostic_source = ann.span.file.diagnostic_source(resolver);
ResolvedAnnotation::new(path, &diagnostic_source, ann, resolver)
})
Expand Down
23 changes: 21 additions & 2 deletions crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ use itertools::Itertools;
use log::debug;
use rustc_hash::{FxHashMap, FxHashSet};

use ruff_db::diagnostic::Diagnostic;
use ruff_db::diagnostic::{
Annotation, Diagnostic, IntoDiagnosticMessage, Span, SubDiagnostic, SubDiagnosticSeverity,
};
use ruff_diagnostics::{Applicability, Fix, IsolationLevel};
use ruff_notebook::{CellOffsets, NotebookIndex};
use ruff_python_ast::helpers::{collect_import_from_member, is_docstring_stmt, to_module_path};
Expand Down Expand Up @@ -3305,6 +3307,22 @@ impl DiagnosticGuard<'_, '_> {
Err(err) => log::debug!("Failed to create fix for {}: {}", self.name(), err),
}
}

/// Add an "info" sub-diagnostic with the given message and range.
///
/// Note that this shadows `Diagnostic::info` from the `Deref` implementation because we'll
/// usually want to attach a range here.
pub(crate) fn info<'a>(
&mut self,
message: impl IntoDiagnosticMessage + 'a,
range: impl Ranged,
) {
let mut sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, message);
let span = Span::from(self.context.source_file.clone()).with_range(range.range());
let ann = Annotation::primary(span);
sub.annotate(ann);
self.diagnostic.as_mut().unwrap().sub(sub);
}
}

impl std::ops::Deref for DiagnosticGuard<'_, '_> {
Expand All @@ -3331,7 +3349,8 @@ impl Drop for DiagnosticGuard<'_, '_> {
return;
}

if let Some(diagnostic) = self.diagnostic.take() {
if let Some(mut diagnostic) = self.diagnostic.take() {
diagnostic.sort_sub_diagnostics();
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 sure about this. I think it's important that a lint rule author is in full control about how a diagnostic is rendered. If they decide that a sub diagnostic with lower severity should come first, than so be it. I'm sure there's a reason for it.

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 know what you mean, I was just trying to work around the automatic help sub-diagnostic for every rule that has a fix as easily as possible. I'm assuming we don't want lint authors to have to control that, but maybe we can just expose the sub-diagnostic Vec or an API that lets them insert additional sub-diagnostics wherever they want. Or another level of DiagnosticGuard that pushes the help/fix_title last instead of immediately when the Diagnostic is created from a Violation.

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.

A more strimlined version of report_diagnostic, e.g. report_custom or similar, seems the most reasonable to me. Ideally, we'd just remove fix and help, but I believe those fields are used on the website or other output formats? If not, that would be the simplest: just remove help and fix_title and set them manually

self.context.diagnostics.borrow_mut().push(diagnostic);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,20 @@ pub(crate) fn redefined_while_unused(checker: &Checker, scope_id: ScopeId, scope
// Create diagnostics for each statement.
for (source, entries) in &redefinitions {
for (shadowed, binding) in entries {
let name = binding.name(checker.source());
let mut diagnostic = checker.report_diagnostic(
RedefinedWhileUnused {
name: binding.name(checker.source()).to_string(),
name: name.to_string(),
row: checker.compute_source_row(shadowed.start()),
},
binding.range(),
);

diagnostic.info(
format_args!("previous definition of `{name}` here"),
shadowed,
);

if let Some(range) = binding.parent_range(checker.semantic()) {
diagnostic.set_parent(range.start());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,12 @@ F811 Redefinition of unused `bar` from line 6
| ^^^
11 | pass
|
info: previous definition of `bar` here
--> F811_0.py:6:5
|
5 | @foo
6 | def bar():
| ^^^
7 | pass
|
help: Remove definition: `bar`
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ F811 Redefinition of unused `FU` from line 1
1 | import fu as FU, bar as FU
| ^^
|
info: previous definition of `FU` here
--> F811_1.py:1:14
|
1 | import fu as FU, bar as FU
| ^^
|
help: Remove definition: `FU`
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,13 @@ F811 Redefinition of unused `mixer` from line 2
| ^^^^^
7 | mixer(123)
|
info: previous definition of `mixer` here
--> F811_12.py:2:20
|
1 | try:
2 | from aa import mixer
| ^^^^^
3 | except ImportError:
4 | pass
|
help: Remove definition: `mixer`
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@ F811 Redefinition of unused `fu` from line 1
| ^^
5 | pass
|
info: previous definition of `fu` here
--> F811_15.py:1:8
|
1 | import fu
| ^^
|
help: Remove definition: `fu`
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,12 @@ F811 Redefinition of unused `fu` from line 3
| ^^
9 | pass
|
info: previous definition of `fu` here
--> F811_16.py:3:8
|
1 | """Test that shadowing a global with a nested function generates a warning."""
2 |
3 | import fu
| ^^
|
help: Remove definition: `fu`
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ F811 [*] Redefinition of unused `fu` from line 2
7 |
8 | def baz():
|
info: previous definition of `fu` here
--> F811_17.py:2:8
|
1 | """Test that shadowing a global name with a nested function generates a warning."""
2 | import fu
| ^^
|
help: Remove definition: `fu`

ℹ Safe fix
Expand All @@ -29,4 +36,13 @@ F811 Redefinition of unused `fu` from line 6
| ^^
10 | pass
|
info: previous definition of `fu` here
--> F811_17.py:6:12
|
5 | def bar():
6 | import fu
| ^^
7 |
8 | def baz():
|
help: Remove definition: `fu`
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ F811 Redefinition of unused `FU` from line 1
1 | from moo import fu as FU, bar as FU
| ^^
|
info: previous definition of `FU` here
--> F811_2.py:1:23
|
1 | from moo import fu as FU, bar as FU
| ^^
|
help: Remove definition: `FU`
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ F811 [*] Redefinition of unused `Sequence` from line 26
| ^^^^^^^^
33 | )
|
info: previous definition of `Sequence` here
--> F811_21.py:26:5
|
24 | from typing import (
25 | List, # noqa
26 | Sequence, # noqa
| ^^^^^^^^
27 | )
|
help: Remove definition: `Sequence`

ℹ Safe fix
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,13 @@ F811 Redefinition of unused `foo` from line 3
4 | import bar as foo
| ^^^
|
info: previous definition of `foo` here
--> F811_23.py:3:15
|
1 | """Test that shadowing an explicit re-export produces a warning."""
2 |
3 | import foo as foo
| ^^^
4 | import bar as foo
|
help: Remove definition: `foo`
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,12 @@ F811 Redefinition of unused `func` from line 2
| ^^^^
6 | pass
|
info: previous definition of `func` here
--> F811_26.py:2:9
|
1 | class Class:
2 | def func(self):
| ^^^^
3 | pass
|
help: Remove definition: `func`
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,13 @@ F811 Redefinition of unused `datetime` from line 3
5 |
6 | datetime(1, 2, 3)
|
info: previous definition of `datetime` here
--> F811_28.py:3:8
|
1 | """Regression test for: https://github.com/astral-sh/ruff/issues/10384"""
2 |
3 | import datetime
| ^^^^^^^^
4 | from datetime import datetime
|
help: Remove definition: `datetime`
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,14 @@ F811 Redefinition of unused `Bar` from line 3
8 | Bar = 1 # F811
| ^^^
|
info: previous definition of `Bar` here
--> F811_29.pyi:3:24
|
1 | """Regression test for: https://github.com/astral-sh/ruff/issues/10509"""
2 |
3 | from foo import Bar as Bar
| ^^^
4 |
5 | class Eggs:
|
help: Remove definition: `Bar`
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ F811 Redefinition of unused `fu` from line 1
1 | import fu; fu = 3
| ^^
|
info: previous definition of `fu` here
--> F811_3.py:1:8
|
1 | import fu; fu = 3
| ^^
|
help: Remove definition: `fu`
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ F811 Redefinition of unused `bar` from line 10
| ^^^
13 | """Bar."""
|
info: previous definition of `bar` here
--> F811_30.py:10:5
|
8 | """Foo."""
9 |
10 | bar = foo
| ^^^
11 |
12 | def bar(self) -> None:
|
help: Remove definition: `bar`

F811 Redefinition of unused `baz` from line 18
Expand All @@ -20,6 +30,15 @@ F811 Redefinition of unused `baz` from line 18
21 | baz = 1
| ^^^
|
info: previous definition of `baz` here
--> F811_30.py:18:9
|
16 | class B:
17 | """B."""
18 | def baz(self) -> None:
| ^^^
19 | """Baz."""
|
help: Remove definition: `baz`

F811 Redefinition of unused `foo` from line 26
Expand All @@ -30,4 +49,13 @@ F811 Redefinition of unused `foo` from line 26
29 | bar = (foo := 1)
| ^^^
|
info: previous definition of `foo` here
--> F811_30.py:26:9
|
24 | class C:
25 | """C."""
26 | def foo(self) -> None:
| ^^^
27 | """Foo."""
|
help: Remove definition: `foo`
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,13 @@ F811 Redefinition of unused `baz` from line 17
20 | except ImportError:
21 | pass
|
info: previous definition of `baz` here
--> F811_31.py:17:5
|
16 | try:
17 | baz = None
| ^^^
18 |
19 | from some_module import baz
|
help: Remove definition: `baz`
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ F811 [*] Redefinition of unused `List` from line 4
4 | List,
5 | List,
| ^^^^
6 | )
|
info: previous definition of `List` here
--> F811_32.py:4:5
|
3 | from typing import (
4 | List,
| ^^^^
5 | List,
6 | )
|
help: Remove definition: `List`
Expand Down
Loading
Loading