Move fix suggestion to subdiagnostic (#19464)

Summary
--

This PR tweaks Ruff's internal usage of the new diagnostic model to more
closely
match the intended use, as I understand it. Specifically, it moves the
fix/help
suggestion from the primary annotation's message to a subdiagnostic. In
turn, it
adds the secondary/noqa code as the new primary annotation message. As
shown in
the new `ruff_db` tests, this more closely mirrors Ruff's current
diagnostic
output.

I also added `Severity::Help` to render the fix suggestion with a
`help:` prefix
instead of `info:`.

These changes don't have any external impact now but should help a bit
with #19415.

Test Plan
--

New full output format tests in `ruff_db`

Rendered Diagnostics
--

Full diagnostic output from `annotate-snippets` in this PR:

``` 
error[unused-import]: `os` imported but unused
  --> fib.py:1:8
   |
 1 | import os
   |        ^^
   |
 help: Remove unused import: `os`
```

Current Ruff output for the same code:

```
fib.py:1:8: F401 [*] `os` imported but unused
  |
1 | import os
  |        ^^ F401
  |
  = help: Remove unused import: `os`
```

Proposed final output after #19415:

``` 
F401 [*] `os` imported but unused
  --> fib.py:1:8
   |
 1 | import os
   |        ^^
   |
 help: Remove unused import: `os`
```

These are slightly updated from
https://github.com/astral-sh/ruff/pull/19464#issuecomment-3097377634
below to remove the extra noqa codes in the primary annotation messages
for the first and third cases.
This commit is contained in:
Brent Westbrook 2025-07-22 10:03:58 -04:00 committed by GitHub
parent c82fa94e0a
commit fd335eb8b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 235 additions and 106 deletions

View file

@ -454,7 +454,7 @@ impl LintCacheData {
CacheMessage { CacheMessage {
rule, rule,
body: msg.body().to_string(), body: msg.body().to_string(),
suggestion: msg.suggestion().map(ToString::to_string), suggestion: msg.first_help_text().map(ToString::to_string),
range: msg.expect_range(), range: msg.expect_range(),
parent: msg.parent(), parent: msg.parent(),
fix: msg.fix().cloned(), fix: msg.fix().cloned(),

View file

@ -122,7 +122,14 @@ impl Diagnostic {
/// directly. If callers want or need to avoid cloning the diagnostic /// directly. If callers want or need to avoid cloning the diagnostic
/// message, then they can also pass a `DiagnosticMessage` directly. /// message, then they can also pass a `DiagnosticMessage` directly.
pub fn info<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) { pub fn info<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
self.sub(SubDiagnostic::new(Severity::Info, message)); self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Info, message));
}
/// Adds a "help" sub-diagnostic with the given message.
///
/// See the closely related [`Diagnostic::info`] method for more details.
pub fn help<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Help, message));
} }
/// Adds a "sub" diagnostic to this diagnostic. /// Adds a "sub" diagnostic to this diagnostic.
@ -377,9 +384,15 @@ impl Diagnostic {
self.primary_message() self.primary_message()
} }
/// Returns the fix suggestion for the violation. /// Returns the message of the first sub-diagnostic with a `Help` severity.
pub fn suggestion(&self) -> Option<&str> { ///
self.primary_annotation()?.get_message() /// Note that this is used as the fix title/suggestion for some of Ruff's output formats, but in
/// general this is not the guaranteed meaning of such a message.
pub fn first_help_text(&self) -> Option<&str> {
self.sub_diagnostics()
.iter()
.find(|sub| matches!(sub.inner.severity, SubDiagnosticSeverity::Help))
.map(|sub| sub.inner.message.as_str())
} }
/// Returns the URL for the rule documentation, if it exists. /// Returns the URL for the rule documentation, if it exists.
@ -565,7 +578,10 @@ impl SubDiagnostic {
/// Callers can pass anything that implements `std::fmt::Display` /// Callers can pass anything that implements `std::fmt::Display`
/// directly. If callers want or need to avoid cloning the diagnostic /// directly. If callers want or need to avoid cloning the diagnostic
/// message, then they can also pass a `DiagnosticMessage` directly. /// message, then they can also pass a `DiagnosticMessage` directly.
pub fn new<'a>(severity: Severity, message: impl IntoDiagnosticMessage + 'a) -> SubDiagnostic { pub fn new<'a>(
severity: SubDiagnosticSeverity,
message: impl IntoDiagnosticMessage + 'a,
) -> SubDiagnostic {
let inner = Box::new(SubDiagnosticInner { let inner = Box::new(SubDiagnosticInner {
severity, severity,
message: message.into_diagnostic_message(), message: message.into_diagnostic_message(),
@ -643,7 +659,7 @@ impl SubDiagnostic {
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)] #[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
struct SubDiagnosticInner { struct SubDiagnosticInner {
severity: Severity, severity: SubDiagnosticSeverity,
message: DiagnosticMessage, message: DiagnosticMessage,
annotations: Vec<Annotation>, annotations: Vec<Annotation>,
} }
@ -1170,6 +1186,30 @@ impl Severity {
} }
} }
/// Like [`Severity`] but exclusively for sub-diagnostics.
///
/// This supports an additional `Help` severity that may not be needed in main diagnostics.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
pub enum SubDiagnosticSeverity {
Help,
Info,
Warning,
Error,
Fatal,
}
impl SubDiagnosticSeverity {
fn to_annotate(self) -> AnnotateLevel {
match self {
SubDiagnosticSeverity::Help => AnnotateLevel::Help,
SubDiagnosticSeverity::Info => AnnotateLevel::Info,
SubDiagnosticSeverity::Warning => AnnotateLevel::Warning,
SubDiagnosticSeverity::Error => AnnotateLevel::Error,
SubDiagnosticSeverity::Fatal => AnnotateLevel::Error,
}
}
}
/// Configuration for rendering diagnostics. /// Configuration for rendering diagnostics.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct DisplayDiagnosticConfig { pub struct DisplayDiagnosticConfig {

View file

@ -26,6 +26,7 @@ use azure::AzureRenderer;
use pylint::PylintRenderer; use pylint::PylintRenderer;
mod azure; mod azure;
mod full;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
mod json; mod json;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
@ -256,7 +257,7 @@ impl<'a> Resolved<'a> {
/// both.) /// both.)
#[derive(Debug)] #[derive(Debug)]
struct ResolvedDiagnostic<'a> { struct ResolvedDiagnostic<'a> {
severity: Severity, level: AnnotateLevel,
id: Option<String>, id: Option<String>,
message: String, message: String,
annotations: Vec<ResolvedAnnotation<'a>>, annotations: Vec<ResolvedAnnotation<'a>>,
@ -281,7 +282,7 @@ impl<'a> ResolvedDiagnostic<'a> {
let id = Some(diag.inner.id.to_string()); let id = Some(diag.inner.id.to_string());
let message = diag.inner.message.as_str().to_string(); let message = diag.inner.message.as_str().to_string();
ResolvedDiagnostic { ResolvedDiagnostic {
severity: diag.inner.severity, level: diag.inner.severity.to_annotate(),
id, id,
message, message,
annotations, annotations,
@ -304,7 +305,7 @@ impl<'a> ResolvedDiagnostic<'a> {
}) })
.collect(); .collect();
ResolvedDiagnostic { ResolvedDiagnostic {
severity: diag.inner.severity, level: diag.inner.severity.to_annotate(),
id: None, id: None,
message: diag.inner.message.as_str().to_string(), message: diag.inner.message.as_str().to_string(),
annotations, annotations,
@ -371,7 +372,7 @@ impl<'a> ResolvedDiagnostic<'a> {
snippets_by_input snippets_by_input
.sort_by(|snips1, snips2| snips1.has_primary.cmp(&snips2.has_primary).reverse()); .sort_by(|snips1, snips2| snips1.has_primary.cmp(&snips2.has_primary).reverse());
RenderableDiagnostic { RenderableDiagnostic {
severity: self.severity, level: self.level,
id: self.id.as_deref(), id: self.id.as_deref(),
message: &self.message, message: &self.message,
snippets_by_input, snippets_by_input,
@ -459,7 +460,7 @@ struct Renderable<'r> {
#[derive(Debug)] #[derive(Debug)]
struct RenderableDiagnostic<'r> { struct RenderableDiagnostic<'r> {
/// The severity of the diagnostic. /// The severity of the diagnostic.
severity: Severity, level: AnnotateLevel,
/// The ID of the diagnostic. The ID can usually be used on the CLI or in a /// The ID of the diagnostic. The ID can usually be used on the CLI or in a
/// config file to change the severity of a lint. /// config file to change the severity of a lint.
/// ///
@ -478,7 +479,6 @@ struct RenderableDiagnostic<'r> {
impl RenderableDiagnostic<'_> { impl RenderableDiagnostic<'_> {
/// Convert this to an "annotate" snippet. /// Convert this to an "annotate" snippet.
fn to_annotate(&self) -> AnnotateMessage<'_> { fn to_annotate(&self) -> AnnotateMessage<'_> {
let level = self.severity.to_annotate();
let snippets = self.snippets_by_input.iter().flat_map(|snippets| { let snippets = self.snippets_by_input.iter().flat_map(|snippets| {
let path = snippets.path; let path = snippets.path;
snippets snippets
@ -486,7 +486,7 @@ impl RenderableDiagnostic<'_> {
.iter() .iter()
.map(|snippet| snippet.to_annotate(path)) .map(|snippet| snippet.to_annotate(path))
}); });
let mut message = level.title(self.message); let mut message = self.level.title(self.message);
if let Some(id) = self.id { if let Some(id) = self.id {
message = message.id(id); message = message.id(id);
} }
@ -864,7 +864,10 @@ mod tests {
use ruff_diagnostics::{Edit, Fix}; use ruff_diagnostics::{Edit, Fix};
use crate::diagnostic::{Annotation, DiagnosticId, SecondaryCode, Severity, Span}; use crate::diagnostic::{
Annotation, DiagnosticId, IntoDiagnosticMessage, SecondaryCode, Severity, Span,
SubDiagnosticSeverity,
};
use crate::files::system_path_to_file; use crate::files::system_path_to_file;
use crate::system::{DbWithWritableSystem, SystemPath}; use crate::system::{DbWithWritableSystem, SystemPath};
use crate::tests::TestDb; use crate::tests::TestDb;
@ -1548,7 +1551,7 @@ watermelon
let mut diag = env.err().primary("animals", "3", "3", "").build(); let mut diag = env.err().primary("animals", "3", "3", "").build();
diag.sub( diag.sub(
env.sub_builder(Severity::Info, "this is a helpful note") env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
.build(), .build(),
); );
insta::assert_snapshot!( insta::assert_snapshot!(
@ -1577,15 +1580,15 @@ watermelon
let mut diag = env.err().primary("animals", "3", "3", "").build(); let mut diag = env.err().primary("animals", "3", "3", "").build();
diag.sub( diag.sub(
env.sub_builder(Severity::Info, "this is a helpful note") env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
.build(), .build(),
); );
diag.sub( diag.sub(
env.sub_builder(Severity::Info, "another helpful note") env.sub_builder(SubDiagnosticSeverity::Info, "another helpful note")
.build(), .build(),
); );
diag.sub( diag.sub(
env.sub_builder(Severity::Info, "and another helpful note") env.sub_builder(SubDiagnosticSeverity::Info, "and another helpful note")
.build(), .build(),
); );
insta::assert_snapshot!( insta::assert_snapshot!(
@ -2370,7 +2373,7 @@ watermelon
/// sub-diagnostic with "error" severity and canned values for /// sub-diagnostic with "error" severity and canned values for
/// its identifier and message. /// its identifier and message.
fn sub_warn(&mut self) -> SubDiagnosticBuilder<'_> { fn sub_warn(&mut self) -> SubDiagnosticBuilder<'_> {
self.sub_builder(Severity::Warning, "sub-diagnostic message") self.sub_builder(SubDiagnosticSeverity::Warning, "sub-diagnostic message")
} }
/// Returns a builder for tersely constructing diagnostics. /// Returns a builder for tersely constructing diagnostics.
@ -2391,7 +2394,11 @@ watermelon
} }
/// Returns a builder for tersely constructing sub-diagnostics. /// Returns a builder for tersely constructing sub-diagnostics.
fn sub_builder(&mut self, severity: Severity, message: &str) -> SubDiagnosticBuilder<'_> { fn sub_builder(
&mut self,
severity: SubDiagnosticSeverity,
message: &str,
) -> SubDiagnosticBuilder<'_> {
let subdiag = SubDiagnostic::new(severity, message); let subdiag = SubDiagnostic::new(severity, message);
SubDiagnosticBuilder { env: self, subdiag } SubDiagnosticBuilder { env: self, subdiag }
} }
@ -2494,6 +2501,12 @@ watermelon
self.diag.set_noqa_offset(noqa_offset); self.diag.set_noqa_offset(noqa_offset);
self self
} }
/// Adds a "help" sub-diagnostic with the given message.
fn help(mut self, message: impl IntoDiagnosticMessage) -> DiagnosticBuilder<'e> {
self.diag.help(message);
self
}
} }
/// A helper builder for tersely populating a `SubDiagnostic`. /// A helper builder for tersely populating a `SubDiagnostic`.
@ -2600,7 +2613,8 @@ def fibonacci(n):
let diagnostics = vec![ let diagnostics = vec![
env.builder("unused-import", Severity::Error, "`os` imported but unused") env.builder("unused-import", Severity::Error, "`os` imported but unused")
.primary("fib.py", "1:7", "1:9", "Remove unused import: `os`") .primary("fib.py", "1:7", "1:9", "")
.help("Remove unused import: `os`")
.secondary_code("F401") .secondary_code("F401")
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new( .fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(0), TextSize::from(0),
@ -2613,12 +2627,8 @@ def fibonacci(n):
Severity::Error, Severity::Error,
"Local variable `x` is assigned to but never used", "Local variable `x` is assigned to but never used",
) )
.primary( .primary("fib.py", "6:4", "6:5", "")
"fib.py", .help("Remove assignment to unused variable `x`")
"6:4",
"6:5",
"Remove assignment to unused variable `x`",
)
.secondary_code("F841") .secondary_code("F841")
.fix(Fix::unsafe_edit(Edit::deletion( .fix(Fix::unsafe_edit(Edit::deletion(
TextSize::from(94), TextSize::from(94),
@ -2720,7 +2730,8 @@ if call(foo
let diagnostics = vec![ let diagnostics = vec![
env.builder("unused-import", Severity::Error, "`os` imported but unused") env.builder("unused-import", Severity::Error, "`os` imported but unused")
.primary("notebook.ipynb", "2:7", "2:9", "Remove unused import: `os`") .primary("notebook.ipynb", "2:7", "2:9", "")
.help("Remove unused import: `os`")
.secondary_code("F401") .secondary_code("F401")
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new( .fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(9), TextSize::from(9),
@ -2733,12 +2744,8 @@ if call(foo
Severity::Error, Severity::Error,
"`math` imported but unused", "`math` imported but unused",
) )
.primary( .primary("notebook.ipynb", "4:7", "4:11", "")
"notebook.ipynb", .help("Remove unused import: `math`")
"4:7",
"4:11",
"Remove unused import: `math`",
)
.secondary_code("F401") .secondary_code("F401")
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new( .fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(28), TextSize::from(28),
@ -2751,12 +2758,8 @@ if call(foo
Severity::Error, Severity::Error,
"Local variable `x` is assigned to but never used", "Local variable `x` is assigned to but never used",
) )
.primary( .primary("notebook.ipynb", "10:4", "10:5", "")
"notebook.ipynb", .help("Remove assignment to unused variable `x`")
"10:4",
"10:5",
"Remove assignment to unused variable `x`",
)
.secondary_code("F841") .secondary_code("F841")
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new( .fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(94), TextSize::from(94),

View file

@ -0,0 +1,66 @@
#[cfg(test)]
mod tests {
use crate::diagnostic::{
DiagnosticFormat,
render::tests::{create_diagnostics, create_syntax_error_diagnostics},
};
#[test]
fn output() {
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Full);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r#"
error[unused-import]: `os` imported but unused
--> fib.py:1:8
|
1 | import os
| ^^
|
help: Remove unused import: `os`
error[unused-variable]: Local variable `x` is assigned to but never used
--> fib.py:6:5
|
4 | def fibonacci(n):
5 | """Compute the nth number in the Fibonacci sequence."""
6 | x = 1
| ^
7 | if n == 0:
8 | return 0
|
help: Remove assignment to unused variable `x`
error[undefined-name]: Undefined name `a`
--> undef.py:1:4
|
1 | if a == 1: pass
| ^
|
"#);
}
#[test]
fn syntax_errors() {
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Full);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
error[invalid-syntax]: SyntaxError: Expected one or more symbol names after import
--> syntax_errors.py:1:15
|
1 | from os import
| ^
2 |
3 | if call(foo
|
error[invalid-syntax]: SyntaxError: Expected ')', found newline
--> syntax_errors.py:3:12
|
1 | from os import
2 |
3 | if call(foo
| ^
4 | def bar():
5 | pass
|
");
}
}

View file

@ -87,7 +87,7 @@ pub(super) fn diagnostic_to_json<'a>(
let fix = diagnostic.fix().map(|fix| JsonFix { let fix = diagnostic.fix().map(|fix| JsonFix {
applicability: fix.applicability(), applicability: fix.applicability(),
message: diagnostic.suggestion(), message: diagnostic.first_help_text(),
edits: ExpandedEdits { edits: ExpandedEdits {
edits: fix.edits(), edits: fix.edits(),
notebook_index, notebook_index,

View file

@ -75,12 +75,13 @@ where
); );
let span = Span::from(file).with_range(range); let span = Span::from(file).with_range(range);
let mut annotation = Annotation::primary(span); let annotation = Annotation::primary(span);
if let Some(suggestion) = suggestion {
annotation = annotation.message(suggestion);
}
diagnostic.annotate(annotation); diagnostic.annotate(annotation);
if let Some(suggestion) = suggestion {
diagnostic.help(suggestion);
}
if let Some(fix) = fix { if let Some(fix) = fix {
diagnostic.set_fix(fix); diagnostic.set_fix(fix);
} }

View file

@ -186,7 +186,7 @@ pub(super) struct MessageCodeFrame<'a> {
impl Display for MessageCodeFrame<'_> { impl Display for MessageCodeFrame<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let suggestion = self.message.suggestion(); let suggestion = self.message.first_help_text();
let footers = if let Some(suggestion) = suggestion { let footers = if let Some(suggestion) = suggestion {
vec![Level::Help.title(suggestion)] vec![Level::Help.title(suggestion)]
} else { } else {

View file

@ -272,7 +272,7 @@ Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to e
} }
assert!( assert!(
!(fixable && diagnostic.suggestion().is_none()), !(fixable && diagnostic.first_help_text().is_none()),
"Diagnostic emitted by {rule:?} is fixable but \ "Diagnostic emitted by {rule:?} is fixable but \
`Violation::fix_title` returns `None`" `Violation::fix_title` returns `None`"
); );

View file

@ -238,7 +238,7 @@ fn to_lsp_diagnostic(
let name = diagnostic.name(); let name = diagnostic.name();
let body = diagnostic.body().to_string(); let body = diagnostic.body().to_string();
let fix = diagnostic.fix(); let fix = diagnostic.fix();
let suggestion = diagnostic.suggestion(); let suggestion = diagnostic.first_help_text();
let code = diagnostic.secondary_code(); let code = diagnostic.secondary_code();
let fix = fix.and_then(|fix| fix.applies(Applicability::Unsafe).then_some(fix)); let fix = fix.and_then(|fix| fix.applies(Applicability::Unsafe).then_some(fix));

View file

@ -234,7 +234,7 @@ impl Workspace {
start_location: source_code.line_column(msg.expect_range().start()).into(), start_location: source_code.line_column(msg.expect_range().start()).into(),
end_location: source_code.line_column(msg.expect_range().end()).into(), end_location: source_code.line_column(msg.expect_range().end()).into(),
fix: msg.fix().map(|fix| ExpandedFix { fix: msg.fix().map(|fix| ExpandedFix {
message: msg.suggestion().map(ToString::to_string), message: msg.first_help_text().map(ToString::to_string),
edits: fix edits: fix
.edits() .edits()
.iter() .iter()

View file

@ -32,6 +32,7 @@ mod tests {
use insta::assert_snapshot; use insta::assert_snapshot;
use ruff_db::diagnostic::{ use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
SubDiagnosticSeverity,
}; };
use ruff_db::files::FileRange; use ruff_db::files::FileRange;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -1349,7 +1350,7 @@ class MyClass:
impl IntoDiagnostic for GotoDeclarationDiagnostic { impl IntoDiagnostic for GotoDeclarationDiagnostic {
fn into_diagnostic(self) -> Diagnostic { fn into_diagnostic(self) -> Diagnostic {
let mut source = SubDiagnostic::new(Severity::Info, "Source"); let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source");
source.annotate(Annotation::primary( source.annotate(Annotation::primary(
Span::from(self.source.file()).with_range(self.source.range()), Span::from(self.source.file()).with_range(self.source.range()),
)); ));

View file

@ -37,6 +37,7 @@ mod test {
use insta::assert_snapshot; use insta::assert_snapshot;
use ruff_db::diagnostic::{ use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
SubDiagnosticSeverity,
}; };
use ruff_db::files::FileRange; use ruff_db::files::FileRange;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -575,7 +576,7 @@ class MyClass: ...
impl IntoDiagnostic for GotoDefinitionDiagnostic { impl IntoDiagnostic for GotoDefinitionDiagnostic {
fn into_diagnostic(self) -> Diagnostic { fn into_diagnostic(self) -> Diagnostic {
let mut source = SubDiagnostic::new(Severity::Info, "Source"); let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source");
source.annotate(Annotation::primary( source.annotate(Annotation::primary(
Span::from(self.source.file()).with_range(self.source.range()), Span::from(self.source.file()).with_range(self.source.range()),
)); ));

View file

@ -33,6 +33,7 @@ mod tests {
use insta::assert_snapshot; use insta::assert_snapshot;
use ruff_db::diagnostic::{ use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
SubDiagnosticSeverity,
}; };
use ruff_db::files::FileRange; use ruff_db::files::FileRange;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
@ -640,7 +641,7 @@ f(**kwargs<CURSOR>)
impl IntoDiagnostic for GotoTypeDefinitionDiagnostic { impl IntoDiagnostic for GotoTypeDefinitionDiagnostic {
fn into_diagnostic(self) -> Diagnostic { fn into_diagnostic(self) -> Diagnostic {
let mut source = SubDiagnostic::new(Severity::Info, "Source"); let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source");
source.annotate(Annotation::primary( source.annotate(Annotation::primary(
Span::from(self.source.file()).with_range(self.source.range()), Span::from(self.source.file()).with_range(self.source.range()),
)); ));

View file

@ -5,7 +5,9 @@ pub use db::{ChangeResult, CheckMode, Db, ProjectDatabase, SalsaMemoryDump};
use files::{Index, Indexed, IndexedFiles}; use files::{Index, Indexed, IndexedFiles};
use metadata::settings::Settings; use metadata::settings::Settings;
pub use metadata::{ProjectMetadata, ProjectMetadataError}; pub use metadata::{ProjectMetadata, ProjectMetadataError};
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span, SubDiagnostic}; use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, Severity, Span, SubDiagnostic, SubDiagnosticSeverity,
};
use ruff_db::files::{File, FileRootKind}; use ruff_db::files::{File, FileRootKind};
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
use ruff_db::source::{SourceTextError, source_text}; use ruff_db::source::{SourceTextError, source_text};
@ -674,14 +676,17 @@ where
let mut diagnostic = Diagnostic::new(DiagnosticId::Panic, Severity::Fatal, message); let mut diagnostic = Diagnostic::new(DiagnosticId::Panic, Severity::Fatal, message);
diagnostic.sub(SubDiagnostic::new( diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"This indicates a bug in ty.", "This indicates a bug in ty.",
)); ));
let report_message = "If you could open an issue at https://github.com/astral-sh/ty/issues/new?title=%5Bpanic%5D, we'd be very appreciative!"; let report_message = "If you could open an issue at https://github.com/astral-sh/ty/issues/new?title=%5Bpanic%5D, we'd be very appreciative!";
diagnostic.sub(SubDiagnostic::new(Severity::Info, report_message));
diagnostic.sub(SubDiagnostic::new( diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
report_message,
));
diagnostic.sub(SubDiagnostic::new(
SubDiagnosticSeverity::Info,
format!( format!(
"Platform: {os} {arch}", "Platform: {os} {arch}",
os = std::env::consts::OS, os = std::env::consts::OS,
@ -690,13 +695,13 @@ where
)); ));
if let Some(version) = ruff_db::program_version() { if let Some(version) = ruff_db::program_version() {
diagnostic.sub(SubDiagnostic::new( diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format!("Version: {version}"), format!("Version: {version}"),
)); ));
} }
diagnostic.sub(SubDiagnostic::new( diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format!( format!(
"Args: {args:?}", "Args: {args:?}",
args = std::env::args().collect::<Vec<_>>() args = std::env::args().collect::<Vec<_>>()
@ -707,13 +712,13 @@ where
match backtrace.status() { match backtrace.status() {
BacktraceStatus::Disabled => { BacktraceStatus::Disabled => {
diagnostic.sub(SubDiagnostic::new( diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information", "run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information",
)); ));
} }
BacktraceStatus::Captured => { BacktraceStatus::Captured => {
diagnostic.sub(SubDiagnostic::new( diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format!("Backtrace:\n{backtrace}"), format!("Backtrace:\n{backtrace}"),
)); ));
} }
@ -723,7 +728,10 @@ where
if let Some(backtrace) = error.salsa_backtrace { if let Some(backtrace) = error.salsa_backtrace {
salsa::attach(db, || { salsa::attach(db, || {
diagnostic.sub(SubDiagnostic::new(Severity::Info, backtrace.to_string())); diagnostic.sub(SubDiagnostic::new(
SubDiagnosticSeverity::Info,
backtrace.to_string(),
));
}); });
} }

View file

@ -12,7 +12,7 @@ use ordermap::OrderMap;
use ruff_db::RustDoc; use ruff_db::RustDoc;
use ruff_db::diagnostic::{ use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, Severity, Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, Severity,
Span, SubDiagnostic, Span, SubDiagnostic, SubDiagnosticSeverity,
}; };
use ruff_db::files::system_path_to_file; use ruff_db::files::system_path_to_file;
use ruff_db::system::{System, SystemPath, SystemPathBuf}; use ruff_db::system::{System, SystemPath, SystemPathBuf};
@ -318,7 +318,7 @@ impl Options {
if self.environment.or_default().root.is_some() { if self.environment.or_default().root.is_some() {
diagnostic = diagnostic.sub(SubDiagnostic::new( diagnostic = diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"The `src.root` setting was ignored in favor of the `environment.root` setting", "The `src.root` setting was ignored in favor of the `environment.root` setting",
)); ));
} }
@ -811,7 +811,7 @@ fn build_include_filter(
Severity::Warning, Severity::Warning,
) )
.sub(SubDiagnostic::new( .sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"Remove the `include` option to match all files or add a pattern to match specific files", "Remove the `include` option to match all files or add a pattern to match specific files",
)); ));
@ -853,13 +853,13 @@ fn build_include_filter(
)) ))
} else { } else {
diagnostic.sub(SubDiagnostic::new( diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format!("The pattern is defined in the `{}` option in your configuration file", context.include_name()), format!("The pattern is defined in the `{}` option in your configuration file", context.include_name()),
)) ))
} }
} }
ValueSource::Cli => diagnostic.sub(SubDiagnostic::new( ValueSource::Cli => diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"The pattern was specified on the CLI", "The pattern was specified on the CLI",
)), )),
ValueSource::PythonVSCodeExtension => unreachable!("Can't configure includes from the Python VSCode extension"), ValueSource::PythonVSCodeExtension => unreachable!("Can't configure includes from the Python VSCode extension"),
@ -883,7 +883,7 @@ fn build_include_filter(
Severity::Error, Severity::Error,
); );
Box::new(diagnostic.sub(SubDiagnostic::new( Box::new(diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"Please open an issue on the ty repository and share the patterns that caused the error.", "Please open an issue on the ty repository and share the patterns that caused the error.",
))) )))
}) })
@ -936,13 +936,13 @@ fn build_exclude_filter(
)) ))
} else { } else {
diagnostic.sub(SubDiagnostic::new( diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format!("The pattern is defined in the `{}` option in your configuration file", context.exclude_name()), format!("The pattern is defined in the `{}` option in your configuration file", context.exclude_name()),
)) ))
} }
} }
ValueSource::Cli => diagnostic.sub(SubDiagnostic::new( ValueSource::Cli => diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"The pattern was specified on the CLI", "The pattern was specified on the CLI",
)), )),
ValueSource::PythonVSCodeExtension => unreachable!( ValueSource::PythonVSCodeExtension => unreachable!(
@ -960,7 +960,7 @@ fn build_exclude_filter(
Severity::Error, Severity::Error,
); );
Box::new(diagnostic.sub(SubDiagnostic::new( Box::new(diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"Please open an issue on the ty repository and share the patterns that caused the error.", "Please open an issue on the ty repository and share the patterns that caused the error.",
))) )))
}) })
@ -1197,26 +1197,26 @@ impl RangedValue<OverrideOptions> {
diagnostic = if self.rules.is_none() { diagnostic = if self.rules.is_none() {
diagnostic = diagnostic.sub(SubDiagnostic::new( diagnostic = diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"It has no `rules` table", "It has no `rules` table",
)); ));
diagnostic.sub(SubDiagnostic::new( diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"Add a `[overrides.rules]` table...", "Add a `[overrides.rules]` table...",
)) ))
} else { } else {
diagnostic = diagnostic.sub(SubDiagnostic::new( diagnostic = diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"The rules table is empty", "The rules table is empty",
)); ));
diagnostic.sub(SubDiagnostic::new( diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"Add a rule to `[overrides.rules]` to override specific rules...", "Add a rule to `[overrides.rules]` to override specific rules...",
)) ))
}; };
diagnostic = diagnostic.sub(SubDiagnostic::new( diagnostic = diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"or remove the `[[overrides]]` section if there's nothing to override", "or remove the `[[overrides]]` section if there's nothing to override",
)); ));
@ -1251,23 +1251,23 @@ impl RangedValue<OverrideOptions> {
diagnostic = if self.exclude.is_none() { diagnostic = if self.exclude.is_none() {
diagnostic.sub(SubDiagnostic::new( diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"It has no `include` or `exclude` option restricting the files", "It has no `include` or `exclude` option restricting the files",
)) ))
} else { } else {
diagnostic.sub(SubDiagnostic::new( diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"It has no `include` option and `exclude` is empty", "It has no `include` option and `exclude` is empty",
)) ))
}; };
diagnostic = diagnostic.sub(SubDiagnostic::new( diagnostic = diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"Restrict the files by adding a pattern to `include` or `exclude`...", "Restrict the files by adding a pattern to `include` or `exclude`...",
)); ));
diagnostic = diagnostic.sub(SubDiagnostic::new( diagnostic = diagnostic.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"or remove the `[[overrides]]` section and merge the configuration into the root `[rules]` table if the configuration should apply to all files", "or remove the `[[overrides]]` section and merge the configuration into the root `[rules]` table if the configuration should apply to all files",
)); ));

View file

@ -11,7 +11,7 @@ use diagnostic::{
INVALID_CONTEXT_MANAGER, INVALID_SUPER_ARGUMENT, NOT_ITERABLE, POSSIBLY_UNBOUND_IMPLICIT_CALL, INVALID_CONTEXT_MANAGER, INVALID_SUPER_ARGUMENT, NOT_ITERABLE, POSSIBLY_UNBOUND_IMPLICIT_CALL,
UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS, UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS,
}; };
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic}; use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity};
use ruff_db::files::File; use ruff_db::files::File;
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_python_ast::{self as ast, AnyNodeRef};
@ -7077,7 +7077,7 @@ impl<'db> BoolError<'db> {
not_boolable_type.display(context.db()) not_boolable_type.display(context.db())
)); ));
let mut sub = SubDiagnostic::new( let mut sub = SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"`__bool__` methods must only have a `self` parameter", "`__bool__` methods must only have a `self` parameter",
); );
if let Some((func_span, parameter_span)) = not_boolable_type if let Some((func_span, parameter_span)) = not_boolable_type
@ -7102,7 +7102,7 @@ impl<'db> BoolError<'db> {
not_boolable = not_boolable_type.display(context.db()), not_boolable = not_boolable_type.display(context.db()),
)); ));
let mut sub = SubDiagnostic::new( let mut sub = SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format_args!( format_args!(
"`{return_type}` is not assignable to `bool`", "`{return_type}` is not assignable to `bool`",
return_type = return_type.display(context.db()), return_type = return_type.display(context.db()),
@ -7128,7 +7128,7 @@ impl<'db> BoolError<'db> {
not_boolable_type.display(context.db()) not_boolable_type.display(context.db())
)); ));
let sub = SubDiagnostic::new( let sub = SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format_args!( format_args!(
"`__bool__` on `{}` must be callable", "`__bool__` on `{}` must be callable",
not_boolable_type.display(context.db()) not_boolable_type.display(context.db())

View file

@ -30,7 +30,7 @@ use crate::types::{
MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeMapping, UnionType, MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeMapping, UnionType,
WrapperDescriptorKind, enums, ide_support, todo_type, WrapperDescriptorKind, enums, ide_support, todo_type,
}; };
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic}; use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
use ruff_python_ast as ast; use ruff_python_ast as ast;
/// Binding information for a possible union of callables. At a call site, the arguments must be /// Binding information for a possible union of callables. At a call site, the arguments must be
@ -1668,8 +1668,10 @@ impl<'db> CallableBinding<'db> {
.first() .first()
.and_then(|overload| overload.spans(context.db())) .and_then(|overload| overload.spans(context.db()))
{ {
let mut sub = let mut sub = SubDiagnostic::new(
SubDiagnostic::new(Severity::Info, "First overload defined here"); SubDiagnosticSeverity::Info,
"First overload defined here",
);
sub.annotate(Annotation::primary(spans.signature)); sub.annotate(Annotation::primary(spans.signature));
diag.sub(sub); diag.sub(sub);
} }
@ -1696,7 +1698,7 @@ impl<'db> CallableBinding<'db> {
implementation.and_then(|function| function.spans(context.db())) implementation.and_then(|function| function.spans(context.db()))
{ {
let mut sub = SubDiagnostic::new( let mut sub = SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"Overload implementation defined here", "Overload implementation defined here",
); );
sub.annotate(Annotation::primary(spans.signature)); sub.annotate(Annotation::primary(spans.signature));
@ -2570,8 +2572,10 @@ impl<'db> BindingError<'db> {
overload.parameter_span(context.db(), Some(parameter.index)) overload.parameter_span(context.db(), Some(parameter.index))
}) })
{ {
let mut sub = let mut sub = SubDiagnostic::new(
SubDiagnostic::new(Severity::Info, "Matching overload defined here"); SubDiagnosticSeverity::Info,
"Matching overload defined here",
);
sub.annotate(Annotation::primary(name_span)); sub.annotate(Annotation::primary(name_span));
sub.annotate( sub.annotate(
Annotation::secondary(parameter_span) Annotation::secondary(parameter_span)
@ -2607,7 +2611,8 @@ impl<'db> BindingError<'db> {
} else if let Some((name_span, parameter_span)) = } else if let Some((name_span, parameter_span)) =
callable_ty.parameter_span(context.db(), Some(parameter.index)) callable_ty.parameter_span(context.db(), Some(parameter.index))
{ {
let mut sub = SubDiagnostic::new(Severity::Info, "Function defined here"); let mut sub =
SubDiagnostic::new(SubDiagnosticSeverity::Info, "Function defined here");
sub.annotate(Annotation::primary(name_span)); sub.annotate(Annotation::primary(name_span));
sub.annotate( sub.annotate(
Annotation::secondary(parameter_span).message("Parameter declared here"), Annotation::secondary(parameter_span).message("Parameter declared here"),
@ -2733,7 +2738,10 @@ impl<'db> BindingError<'db> {
let module = parsed_module(context.db(), typevar_definition.file(context.db())) let module = parsed_module(context.db(), typevar_definition.file(context.db()))
.load(context.db()); .load(context.db());
let typevar_range = typevar_definition.full_range(context.db(), &module); let typevar_range = typevar_definition.full_range(context.db(), &module);
let mut sub = SubDiagnostic::new(Severity::Info, "Type variable defined here"); let mut sub = SubDiagnostic::new(
SubDiagnosticSeverity::Info,
"Type variable defined here",
);
sub.annotate(Annotation::primary(typevar_range.into())); sub.annotate(Annotation::primary(typevar_range.into()));
diag.sub(sub); diag.sub(sub);
} }
@ -2801,7 +2809,7 @@ impl UnionDiagnostic<'_, '_> {
/// diagnostic. /// diagnostic.
fn add_union_context(&self, db: &'_ dyn Db, diag: &mut Diagnostic) { fn add_union_context(&self, db: &'_ dyn Db, diag: &mut Diagnostic) {
let sub = SubDiagnostic::new( let sub = SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format_args!( format_args!(
"Union variant `{callable_ty}` is incompatible with this call site", "Union variant `{callable_ty}` is incompatible with this call site",
callable_ty = self.binding.callable_type.display(db), callable_ty = self.binding.callable_type.display(db),
@ -2810,7 +2818,7 @@ impl UnionDiagnostic<'_, '_> {
diag.sub(sub); diag.sub(sub);
let sub = SubDiagnostic::new( let sub = SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format_args!( format_args!(
"Attempted to call union type `{}`", "Attempted to call union type `{}`",
self.callable_type.display(db) self.callable_type.display(db)

View file

@ -1,7 +1,7 @@
use std::fmt; use std::fmt;
use drop_bomb::DebugDropBomb; use drop_bomb::DebugDropBomb;
use ruff_db::diagnostic::{DiagnosticTag, SubDiagnostic}; use ruff_db::diagnostic::{DiagnosticTag, SubDiagnostic, SubDiagnosticSeverity};
use ruff_db::parsed::ParsedModuleRef; use ruff_db::parsed::ParsedModuleRef;
use ruff_db::{ use ruff_db::{
diagnostic::{Annotation, Diagnostic, DiagnosticId, IntoDiagnosticMessage, Severity, Span}, diagnostic::{Annotation, Diagnostic, DiagnosticId, IntoDiagnosticMessage, Severity, Span},
@ -330,7 +330,7 @@ impl Drop for LintDiagnosticGuard<'_, '_> {
let mut diag = self.diag.take().unwrap(); let mut diag = self.diag.take().unwrap();
diag.sub(SubDiagnostic::new( diag.sub(SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
match self.source { match self.source {
LintSource::Default => format!("rule `{}` is enabled by default", diag.id()), LintSource::Default => format!("rule `{}` is enabled by default", diag.id()),
LintSource::Cli => format!("rule `{}` was selected on the command line", diag.id()), LintSource::Cli => format!("rule `{}` was selected on the command line", diag.id()),

View file

@ -20,7 +20,7 @@ use crate::types::{SpecialFormType, Type, protocol_class::ProtocolClassLiteral};
use crate::util::diagnostics::format_enumeration; use crate::util::diagnostics::format_enumeration;
use crate::{Db, FxIndexMap, Module, ModuleName, Program, declare_lint}; use crate::{Db, FxIndexMap, Module, ModuleName, Program, declare_lint};
use itertools::Itertools; use itertools::Itertools;
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic}; use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
@ -1929,7 +1929,7 @@ pub(super) fn report_implicit_return_type(
)); ));
let mut sub_diagnostic = SubDiagnostic::new( let mut sub_diagnostic = SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"Only classes that directly inherit from `typing.Protocol` \ "Only classes that directly inherit from `typing.Protocol` \
or `typing_extensions.Protocol` are considered protocol classes", or `typing_extensions.Protocol` are considered protocol classes",
); );
@ -2037,7 +2037,7 @@ pub(crate) fn report_instance_layout_conflict(
)); ));
let mut subdiagnostic = SubDiagnostic::new( let mut subdiagnostic = SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
"Two classes cannot coexist in a class's MRO if their instances \ "Two classes cannot coexist in a class's MRO if their instances \
have incompatible memory layouts", have incompatible memory layouts",
); );
@ -2230,7 +2230,7 @@ pub(crate) fn report_bad_argument_to_get_protocol_members(
diagnostic.info("Only protocol classes can be passed to `get_protocol_members`"); diagnostic.info("Only protocol classes can be passed to `get_protocol_members`");
let mut class_def_diagnostic = SubDiagnostic::new( let mut class_def_diagnostic = SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format_args!( format_args!(
"`{}` is declared here, but it is not a protocol class:", "`{}` is declared here, but it is not a protocol class:",
class.name(db) class.name(db)
@ -2292,7 +2292,7 @@ pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol(
diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); diagnostic.set_primary_message("This call will raise `TypeError` at runtime");
let mut class_def_diagnostic = SubDiagnostic::new( let mut class_def_diagnostic = SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format_args!( format_args!(
"`{class_name}` is declared as a protocol class, \ "`{class_name}` is declared as a protocol class, \
but it is not declared as runtime-checkable" but it is not declared as runtime-checkable"
@ -2326,7 +2326,7 @@ pub(crate) fn report_attempted_protocol_instantiation(
diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); diagnostic.set_primary_message("This call will raise `TypeError` at runtime");
let mut class_def_diagnostic = SubDiagnostic::new( let mut class_def_diagnostic = SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format_args!("Protocol classes cannot be instantiated"), format_args!("Protocol classes cannot be instantiated"),
); );
class_def_diagnostic.annotate( class_def_diagnostic.annotate(
@ -2360,7 +2360,7 @@ pub(crate) fn report_duplicate_bases(
builder.into_diagnostic(format_args!("Duplicate base class `{duplicate_name}`",)); builder.into_diagnostic(format_args!("Duplicate base class `{duplicate_name}`",));
let mut sub_diagnostic = SubDiagnostic::new( let mut sub_diagnostic = SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format_args!( format_args!(
"The definition of class `{}` will raise `TypeError` at runtime", "The definition of class `{}` will raise `TypeError` at runtime",
class.name(db) class.name(db)

View file

@ -1,5 +1,5 @@
use crate::{Db, Program, PythonVersionWithSource}; use crate::{Db, Program, PythonVersionWithSource};
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, SubDiagnostic}; use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
use std::fmt::Write; use std::fmt::Write;
/// Add a subdiagnostic to `diagnostic` that explains why a certain Python version was inferred. /// Add a subdiagnostic to `diagnostic` that explains why a certain Python version was inferred.
@ -23,7 +23,7 @@ pub fn add_inferred_python_version_hint_to_diagnostic(
crate::PythonVersionSource::ConfigFile(source) => { crate::PythonVersionSource::ConfigFile(source) => {
if let Some(span) = source.span(db) { if let Some(span) = source.span(db) {
let mut sub_diagnostic = SubDiagnostic::new( let mut sub_diagnostic = SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format_args!("Python {version} was assumed when {action}"), format_args!("Python {version} was assumed when {action}"),
); );
sub_diagnostic.annotate(Annotation::primary(span).message(format_args!( sub_diagnostic.annotate(Annotation::primary(span).message(format_args!(
@ -39,7 +39,7 @@ pub fn add_inferred_python_version_hint_to_diagnostic(
crate::PythonVersionSource::PyvenvCfgFile(source) => { crate::PythonVersionSource::PyvenvCfgFile(source) => {
if let Some(span) = source.span(db) { if let Some(span) = source.span(db) {
let mut sub_diagnostic = SubDiagnostic::new( let mut sub_diagnostic = SubDiagnostic::new(
Severity::Info, SubDiagnosticSeverity::Info,
format_args!( format_args!(
"Python {version} was assumed when {action} because of your virtual environment" "Python {version} was assumed when {action} because of your virtual environment"
), ),