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 {
rule,
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(),
parent: msg.parent(),
fix: msg.fix().cloned(),

View file

@ -122,7 +122,14 @@ impl Diagnostic {
/// directly. If callers want or need to avoid cloning the diagnostic
/// message, then they can also pass a `DiagnosticMessage` directly.
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.
@ -377,9 +384,15 @@ impl Diagnostic {
self.primary_message()
}
/// Returns the fix suggestion for the violation.
pub fn suggestion(&self) -> Option<&str> {
self.primary_annotation()?.get_message()
/// Returns the message of the first sub-diagnostic with a `Help` severity.
///
/// 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.
@ -565,7 +578,10 @@ impl SubDiagnostic {
/// Callers can pass anything that implements `std::fmt::Display`
/// directly. If callers want or need to avoid cloning the diagnostic
/// 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 {
severity,
message: message.into_diagnostic_message(),
@ -643,7 +659,7 @@ impl SubDiagnostic {
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
struct SubDiagnosticInner {
severity: Severity,
severity: SubDiagnosticSeverity,
message: DiagnosticMessage,
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.
#[derive(Clone, Debug)]
pub struct DisplayDiagnosticConfig {

View file

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

View file

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

View file

@ -186,7 +186,7 @@ pub(super) struct MessageCodeFrame<'a> {
impl Display for MessageCodeFrame<'_> {
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 {
vec![Level::Help.title(suggestion)]
} else {

View file

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

View file

@ -238,7 +238,7 @@ fn to_lsp_diagnostic(
let name = diagnostic.name();
let body = diagnostic.body().to_string();
let fix = diagnostic.fix();
let suggestion = diagnostic.suggestion();
let suggestion = diagnostic.first_help_text();
let code = diagnostic.secondary_code();
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(),
end_location: source_code.line_column(msg.expect_range().end()).into(),
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()
.iter()

View file

@ -32,6 +32,7 @@ mod tests {
use insta::assert_snapshot;
use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
SubDiagnosticSeverity,
};
use ruff_db::files::FileRange;
use ruff_text_size::Ranged;
@ -1349,7 +1350,7 @@ class MyClass:
impl IntoDiagnostic for GotoDeclarationDiagnostic {
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(
Span::from(self.source.file()).with_range(self.source.range()),
));

View file

@ -37,6 +37,7 @@ mod test {
use insta::assert_snapshot;
use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
SubDiagnosticSeverity,
};
use ruff_db::files::FileRange;
use ruff_text_size::Ranged;
@ -575,7 +576,7 @@ class MyClass: ...
impl IntoDiagnostic for GotoDefinitionDiagnostic {
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(
Span::from(self.source.file()).with_range(self.source.range()),
));

View file

@ -33,6 +33,7 @@ mod tests {
use insta::assert_snapshot;
use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
SubDiagnosticSeverity,
};
use ruff_db::files::FileRange;
use ruff_text_size::Ranged;
@ -640,7 +641,7 @@ f(**kwargs<CURSOR>)
impl IntoDiagnostic for GotoTypeDefinitionDiagnostic {
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(
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 metadata::settings::Settings;
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::parsed::parsed_module;
use ruff_db::source::{SourceTextError, source_text};
@ -674,14 +676,17 @@ where
let mut diagnostic = Diagnostic::new(DiagnosticId::Panic, Severity::Fatal, message);
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"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!";
diagnostic.sub(SubDiagnostic::new(Severity::Info, report_message));
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
report_message,
));
diagnostic.sub(SubDiagnostic::new(
SubDiagnosticSeverity::Info,
format!(
"Platform: {os} {arch}",
os = std::env::consts::OS,
@ -690,13 +695,13 @@ where
));
if let Some(version) = ruff_db::program_version() {
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format!("Version: {version}"),
));
}
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format!(
"Args: {args:?}",
args = std::env::args().collect::<Vec<_>>()
@ -707,13 +712,13 @@ where
match backtrace.status() {
BacktraceStatus::Disabled => {
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information",
));
}
BacktraceStatus::Captured => {
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format!("Backtrace:\n{backtrace}"),
));
}
@ -723,7 +728,10 @@ where
if let Some(backtrace) = error.salsa_backtrace {
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::diagnostic::{
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, Severity,
Span, SubDiagnostic,
Span, SubDiagnostic, SubDiagnosticSeverity,
};
use ruff_db::files::system_path_to_file;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
@ -318,7 +318,7 @@ impl Options {
if self.environment.or_default().root.is_some() {
diagnostic = diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"The `src.root` setting was ignored in favor of the `environment.root` setting",
));
}
@ -811,7 +811,7 @@ fn build_include_filter(
Severity::Warning,
)
.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"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 {
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format!("The pattern is defined in the `{}` option in your configuration file", context.include_name()),
))
}
}
ValueSource::Cli => diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"The pattern was specified on the CLI",
)),
ValueSource::PythonVSCodeExtension => unreachable!("Can't configure includes from the Python VSCode extension"),
@ -883,7 +883,7 @@ fn build_include_filter(
Severity::Error,
);
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.",
)))
})
@ -936,13 +936,13 @@ fn build_exclude_filter(
))
} else {
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format!("The pattern is defined in the `{}` option in your configuration file", context.exclude_name()),
))
}
}
ValueSource::Cli => diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"The pattern was specified on the CLI",
)),
ValueSource::PythonVSCodeExtension => unreachable!(
@ -960,7 +960,7 @@ fn build_exclude_filter(
Severity::Error,
);
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.",
)))
})
@ -1197,26 +1197,26 @@ impl RangedValue<OverrideOptions> {
diagnostic = if self.rules.is_none() {
diagnostic = diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"It has no `rules` table",
));
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"Add a `[overrides.rules]` table...",
))
} else {
diagnostic = diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"The rules table is empty",
));
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"Add a rule to `[overrides.rules]` to override specific rules...",
))
};
diagnostic = diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"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.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"It has no `include` or `exclude` option restricting the files",
))
} else {
diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"It has no `include` option and `exclude` is empty",
))
};
diagnostic = diagnostic.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"Restrict the files by adding a pattern to `include` or `exclude`...",
));
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",
));

View file

@ -11,7 +11,7 @@ use diagnostic::{
INVALID_CONTEXT_MANAGER, INVALID_SUPER_ARGUMENT, NOT_ITERABLE, POSSIBLY_UNBOUND_IMPLICIT_CALL,
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_python_ast::name::Name;
use ruff_python_ast::{self as ast, AnyNodeRef};
@ -7077,7 +7077,7 @@ impl<'db> BoolError<'db> {
not_boolable_type.display(context.db())
));
let mut sub = SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"`__bool__` methods must only have a `self` parameter",
);
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()),
));
let mut sub = SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format_args!(
"`{return_type}` is not assignable to `bool`",
return_type = return_type.display(context.db()),
@ -7128,7 +7128,7 @@ impl<'db> BoolError<'db> {
not_boolable_type.display(context.db())
));
let sub = SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format_args!(
"`__bool__` on `{}` must be callable",
not_boolable_type.display(context.db())

View file

@ -30,7 +30,7 @@ use crate::types::{
MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeMapping, UnionType,
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;
/// 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()
.and_then(|overload| overload.spans(context.db()))
{
let mut sub =
SubDiagnostic::new(Severity::Info, "First overload defined here");
let mut sub = SubDiagnostic::new(
SubDiagnosticSeverity::Info,
"First overload defined here",
);
sub.annotate(Annotation::primary(spans.signature));
diag.sub(sub);
}
@ -1696,7 +1698,7 @@ impl<'db> CallableBinding<'db> {
implementation.and_then(|function| function.spans(context.db()))
{
let mut sub = SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"Overload implementation defined here",
);
sub.annotate(Annotation::primary(spans.signature));
@ -2570,8 +2572,10 @@ impl<'db> BindingError<'db> {
overload.parameter_span(context.db(), Some(parameter.index))
})
{
let mut sub =
SubDiagnostic::new(Severity::Info, "Matching overload defined here");
let mut sub = SubDiagnostic::new(
SubDiagnosticSeverity::Info,
"Matching overload defined here",
);
sub.annotate(Annotation::primary(name_span));
sub.annotate(
Annotation::secondary(parameter_span)
@ -2607,7 +2611,8 @@ impl<'db> BindingError<'db> {
} else if let Some((name_span, parameter_span)) =
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::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()))
.load(context.db());
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()));
diag.sub(sub);
}
@ -2801,7 +2809,7 @@ impl UnionDiagnostic<'_, '_> {
/// diagnostic.
fn add_union_context(&self, db: &'_ dyn Db, diag: &mut Diagnostic) {
let sub = SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format_args!(
"Union variant `{callable_ty}` is incompatible with this call site",
callable_ty = self.binding.callable_type.display(db),
@ -2810,7 +2818,7 @@ impl UnionDiagnostic<'_, '_> {
diag.sub(sub);
let sub = SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format_args!(
"Attempted to call union type `{}`",
self.callable_type.display(db)

View file

@ -1,7 +1,7 @@
use std::fmt;
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::{
diagnostic::{Annotation, Diagnostic, DiagnosticId, IntoDiagnosticMessage, Severity, Span},
@ -330,7 +330,7 @@ impl Drop for LintDiagnosticGuard<'_, '_> {
let mut diag = self.diag.take().unwrap();
diag.sub(SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
match self.source {
LintSource::Default => format!("rule `{}` is enabled by default", 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::{Db, FxIndexMap, Module, ModuleName, Program, declare_lint};
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_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet;
@ -1929,7 +1929,7 @@ pub(super) fn report_implicit_return_type(
));
let mut sub_diagnostic = SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"Only classes that directly inherit from `typing.Protocol` \
or `typing_extensions.Protocol` are considered protocol classes",
);
@ -2037,7 +2037,7 @@ pub(crate) fn report_instance_layout_conflict(
));
let mut subdiagnostic = SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
"Two classes cannot coexist in a class's MRO if their instances \
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`");
let mut class_def_diagnostic = SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format_args!(
"`{}` is declared here, but it is not a protocol class:",
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");
let mut class_def_diagnostic = SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format_args!(
"`{class_name}` is declared as a protocol class, \
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");
let mut class_def_diagnostic = SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format_args!("Protocol classes cannot be instantiated"),
);
class_def_diagnostic.annotate(
@ -2360,7 +2360,7 @@ pub(crate) fn report_duplicate_bases(
builder.into_diagnostic(format_args!("Duplicate base class `{duplicate_name}`",));
let mut sub_diagnostic = SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format_args!(
"The definition of class `{}` will raise `TypeError` at runtime",
class.name(db)

View file

@ -1,5 +1,5 @@
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;
/// 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) => {
if let Some(span) = source.span(db) {
let mut sub_diagnostic = SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format_args!("Python {version} was assumed when {action}"),
);
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) => {
if let Some(span) = source.span(db) {
let mut sub_diagnostic = SubDiagnostic::new(
Severity::Info,
SubDiagnosticSeverity::Info,
format_args!(
"Python {version} was assumed when {action} because of your virtual environment"
),