mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:48:32 +00:00
ruff_db: delete old diagnostic renderer
... and switch to the new one. We do this switch by converting the old diagnostics to a `Diagnostic`, and then rendering that. This does not quite emit identical output. There are some changes. They *could* be fixed to remain the same, but the changes aren't obviously worse to me and I think the right way to *improve* them is to move Red Knot to the new `Diagnostic` API. The next commit will have the snapshot changes.
This commit is contained in:
parent
9291074ba6
commit
6883c1dde7
1 changed files with 52 additions and 154 deletions
|
@ -1,15 +1,12 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use ruff_annotate_snippets::{
|
||||
Annotation as AnnotateAnnotation, Level as AnnotateLevel, Message as AnnotateMessage,
|
||||
Renderer as AnnotateRenderer, Snippet as AnnotateSnippet,
|
||||
};
|
||||
use ruff_python_parser::ParseError;
|
||||
use ruff_source_file::{OneIndexed, SourceCode};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::{
|
||||
diagnostic::{DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, Severity, Span},
|
||||
diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, Severity,
|
||||
Span, SubDiagnostic,
|
||||
},
|
||||
files::File,
|
||||
source::{line_index, source_text},
|
||||
Db,
|
||||
|
@ -96,158 +93,59 @@ impl std::fmt::Display for OldDisplayDiagnostic<'_, '_, '_> {
|
|||
return write!(f, " {message}", message = self.diagnostic.message());
|
||||
}
|
||||
|
||||
let render = |f: &mut std::fmt::Formatter, message| {
|
||||
let renderer = if self.config.color {
|
||||
AnnotateRenderer::styled()
|
||||
} else {
|
||||
AnnotateRenderer::plain()
|
||||
let mut diag = match self.diagnostic.span() {
|
||||
None => {
|
||||
// NOTE: This is pretty sub-optimal. It doesn't render well. We
|
||||
// really want a snippet, but without a `File`, we can't really
|
||||
// render anything. It looks like this case currently happens
|
||||
// for configuration errors. It looks like we can probably
|
||||
// produce a snippet for this if it comes from a file, but if
|
||||
// it comes from the CLI, I'm not quite sure exactly what to
|
||||
// do. ---AG
|
||||
Diagnostic::new(
|
||||
self.diagnostic.id(),
|
||||
self.diagnostic.severity(),
|
||||
self.diagnostic.message(),
|
||||
)
|
||||
}
|
||||
Some(span) => {
|
||||
let mut diag =
|
||||
Diagnostic::new(self.diagnostic.id(), self.diagnostic.severity(), "");
|
||||
diag.annotate(Annotation::primary(span).message(self.diagnostic.message()));
|
||||
diag
|
||||
}
|
||||
.cut_indicator("…");
|
||||
let rendered = renderer.render(message);
|
||||
writeln!(f, "{rendered}")
|
||||
};
|
||||
let Some(span) = self.diagnostic.span() else {
|
||||
// NOTE: This is pretty sub-optimal. It doesn't render well. We
|
||||
// really want a snippet, but without a `File`, we can't really
|
||||
// render anything. It looks like this case currently happens
|
||||
// for configuration errors. It looks like we can probably
|
||||
// produce a snippet for this if it comes from a file, but if
|
||||
// it comes from the CLI, I'm not quite sure exactly what to
|
||||
// do. ---AG
|
||||
let msg = format!("{}: {}", self.diagnostic.id(), self.diagnostic.message());
|
||||
return render(f, self.diagnostic.severity().to_annotate().title(&msg));
|
||||
};
|
||||
|
||||
let mut message = Message::new(self.diagnostic.severity(), self.diagnostic.id());
|
||||
message.add_snippet(Snippet::new(
|
||||
self.db,
|
||||
self.diagnostic.severity(),
|
||||
&span,
|
||||
&self.diagnostic.message(),
|
||||
));
|
||||
for secondary_msg in self.diagnostic.secondary_messages() {
|
||||
message.add_snippet(Snippet::new(
|
||||
self.db,
|
||||
Severity::Info,
|
||||
&secondary_msg.span,
|
||||
&secondary_msg.message,
|
||||
));
|
||||
}
|
||||
render(f, message.to_annotate())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Message {
|
||||
level: AnnotateLevel,
|
||||
title: String,
|
||||
snippets: Vec<Snippet>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Snippet {
|
||||
source: String,
|
||||
origin: String,
|
||||
line_start: usize,
|
||||
annotation: Option<Annotation>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Annotation {
|
||||
level: AnnotateLevel,
|
||||
span: TextRange,
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
fn new(severity: Severity, id: DiagnosticId) -> Message {
|
||||
Message {
|
||||
level: severity.to_annotate(),
|
||||
title: id.to_string(),
|
||||
snippets: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn add_snippet(&mut self, snippet: Snippet) {
|
||||
self.snippets.push(snippet);
|
||||
}
|
||||
|
||||
fn to_annotate(&self) -> AnnotateMessage<'_> {
|
||||
self.level
|
||||
.title(&self.title)
|
||||
.snippets(self.snippets.iter().map(|snippet| snippet.to_annotate()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Snippet {
|
||||
fn new(db: &'_ dyn Db, severity: Severity, span: &Span, message: &str) -> Snippet {
|
||||
let origin = span.file.path(db).to_string();
|
||||
let source_text = source_text(db, span.file);
|
||||
let Some(range) = span.range else {
|
||||
return Snippet {
|
||||
source: source_text.to_string(),
|
||||
origin,
|
||||
line_start: 1,
|
||||
annotation: None,
|
||||
};
|
||||
};
|
||||
|
||||
// The bits below are a simplified copy from
|
||||
// `crates/ruff_linter/src/message/text.rs`.
|
||||
let index = line_index(db, span.file);
|
||||
let source_code = SourceCode::new(source_text.as_str(), &index);
|
||||
|
||||
let content_start_index = source_code.line_index(range.start());
|
||||
let mut start_index = content_start_index.saturating_sub(2);
|
||||
// Trim leading empty lines.
|
||||
while start_index < content_start_index {
|
||||
if !source_code.line_text(start_index).trim().is_empty() {
|
||||
break;
|
||||
}
|
||||
start_index = start_index.saturating_add(1);
|
||||
// Secondary messages carry one span and a message
|
||||
// attached to that span. Since we also want them to
|
||||
// appear after the primary diagnostic, we encode them as
|
||||
// sub-diagnostics.
|
||||
//
|
||||
// Moreover, since we only have one message, we attach it
|
||||
// to the annotation and leave the sub-diagnostic message
|
||||
// empty. This leads to somewhat awkward rendering, but
|
||||
// the way to fix that is to migrate Red Knot to the more
|
||||
// expressive `Diagnostic` API.
|
||||
let mut sub = SubDiagnostic::new(Severity::Info, "");
|
||||
sub.annotate(
|
||||
Annotation::secondary(secondary_msg.span.clone()).message(&secondary_msg.message),
|
||||
);
|
||||
diag.sub(sub);
|
||||
}
|
||||
|
||||
let content_end_index = source_code.line_index(range.end());
|
||||
let mut end_index = content_end_index
|
||||
.saturating_add(2)
|
||||
.min(OneIndexed::from_zero_indexed(index.line_count()));
|
||||
// Trim trailing empty lines.
|
||||
while end_index > content_end_index {
|
||||
if !source_code.line_text(end_index).trim().is_empty() {
|
||||
break;
|
||||
}
|
||||
end_index = end_index.saturating_sub(1);
|
||||
}
|
||||
|
||||
// Slice up the code frame and adjust our range.
|
||||
let start_offset = source_code.line_start(start_index);
|
||||
let end_offset = source_code.line_end(end_index);
|
||||
let frame = source_code.slice(TextRange::new(start_offset, end_offset));
|
||||
let range = range - start_offset;
|
||||
|
||||
Snippet {
|
||||
source: frame.to_string(),
|
||||
origin,
|
||||
line_start: start_index.get(),
|
||||
annotation: Some(Annotation {
|
||||
level: severity.to_annotate(),
|
||||
span: range,
|
||||
label: message.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_annotate(&self) -> AnnotateSnippet<'_> {
|
||||
AnnotateSnippet::source(&self.source)
|
||||
.origin(&self.origin)
|
||||
.line_start(self.line_start)
|
||||
.annotations(self.annotation.as_ref().map(|a| a.to_annotate()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Annotation {
|
||||
fn to_annotate(&self) -> AnnotateAnnotation<'_> {
|
||||
self.level.span(self.span.into()).label(&self.label)
|
||||
// The main way to print a `Diagnostic` is via its `print`
|
||||
// method, which specifically goes to a `std::io::Write` in
|
||||
// order to strongly suggest that it is actually emitted
|
||||
// somewhere. We should probably make callers use *that* API
|
||||
// instead of this `Display` impl, but for now, we keep the API
|
||||
// the same and write to a `Vec<u8>` under the covers.
|
||||
let mut buf = vec![];
|
||||
// OK because printing to a `Vec<u8>` will never return an error.
|
||||
diag.print(self.db, self.config, &mut buf)
|
||||
.map_err(|_| std::fmt::Error)?;
|
||||
// OK because diagnostic rendering will always emit valid UTF-8.
|
||||
let string = String::from_utf8(buf).map_err(|_| std::fmt::Error)?;
|
||||
write!(f, "{string}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue