mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-07 21:25:08 +00:00

## Summary This PR is a collaboration with @AlexWaygood from our pairing session last Friday. The main goal here is removing `ruff_linter::message::OldDiagnostic` in favor of using `ruff_db::diagnostic::Diagnostic` directly. This involved a few major steps: - Transferring the fields - Transferring the methods and trait implementations, where possible - Converting some constructor methods to free functions - Moving the `SecondaryCode` struct - Updating the method names I'm hoping that some of the methods, especially those in the `expect_ruff_*` family, won't be necessary long-term, but I avoided trying to replace them entirely for now to keep the already-large diff a bit smaller. ### Related refactors Alex and I noticed a few refactoring opportunities while looking at the code, specifically the very similar implementations for `create_parse_diagnostic`, `create_unsupported_syntax_diagnostic`, and `create_semantic_syntax_diagnostic`. We combined these into a single generic function, which I then copied into `ruff_linter::message` with some small changes and a TODO to combine them in the future. I also deleted the `DisplayParseErrorType` and `TruncateAtNewline` types for reporting parse errors. These were added in #4124, I believe to work around the error messages from LALRPOP. Removing these didn't affect any tests, so I think they were unnecessary now that we fully control the error messages from the parser. On a more minor note, I factored out some calls to the `OldDiagnostic::filename` (now `Diagnostic::expect_ruff_filename`) function to avoid repeatedly allocating `String`s in some places. ### Snapshot changes The `show_statistics_syntax_errors` integration test changed because the `OldDiagnostic::name` method used `syntax-error` instead of `invalid-syntax` like in ty. I think this (`--statistics`) is one of the only places we actually use this name for syntax errors, so I hope this is okay. An alternative is to use `syntax-error` in ty too. The other snapshot changes are from removing this code, as discussed on [Discord](1388252408
):34052a1185/crates/ruff_linter/src/message/mod.rs (L128-L135)
I think both of these are technically breaking changes, but they only affect syntax errors and are very narrow in scope, while also pretty substantially simplifying the refactor, so I hope they're okay to include in a patch release. ## Test plan Existing tests, with the adjustments mentioned above --------- Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
220 lines
7.2 KiB
Rust
220 lines
7.2 KiB
Rust
use std::io::Write;
|
|
|
|
use serde::ser::SerializeSeq;
|
|
use serde::{Serialize, Serializer};
|
|
use serde_json::{Value, json};
|
|
|
|
use ruff_db::diagnostic::Diagnostic;
|
|
use ruff_notebook::NotebookIndex;
|
|
use ruff_source_file::{LineColumn, OneIndexed, SourceCode};
|
|
use ruff_text_size::Ranged;
|
|
|
|
use crate::Edit;
|
|
use crate::message::{Emitter, EmitterContext};
|
|
|
|
#[derive(Default)]
|
|
pub struct JsonEmitter;
|
|
|
|
impl Emitter for JsonEmitter {
|
|
fn emit(
|
|
&mut self,
|
|
writer: &mut dyn Write,
|
|
diagnostics: &[Diagnostic],
|
|
context: &EmitterContext,
|
|
) -> anyhow::Result<()> {
|
|
serde_json::to_writer_pretty(
|
|
writer,
|
|
&ExpandedMessages {
|
|
diagnostics,
|
|
context,
|
|
},
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
struct ExpandedMessages<'a> {
|
|
diagnostics: &'a [Diagnostic],
|
|
context: &'a EmitterContext<'a>,
|
|
}
|
|
|
|
impl Serialize for ExpandedMessages<'_> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let mut s = serializer.serialize_seq(Some(self.diagnostics.len()))?;
|
|
|
|
for message in self.diagnostics {
|
|
let value = message_to_json_value(message, self.context);
|
|
s.serialize_element(&value)?;
|
|
}
|
|
|
|
s.end()
|
|
}
|
|
}
|
|
|
|
pub(crate) fn message_to_json_value(message: &Diagnostic, context: &EmitterContext) -> Value {
|
|
let source_file = message.expect_ruff_source_file();
|
|
let source_code = source_file.to_source_code();
|
|
let filename = message.expect_ruff_filename();
|
|
let notebook_index = context.notebook_index(&filename);
|
|
|
|
let fix = message.fix().map(|fix| {
|
|
json!({
|
|
"applicability": fix.applicability(),
|
|
"message": message.suggestion(),
|
|
"edits": &ExpandedEdits { edits: fix.edits(), source_code: &source_code, notebook_index },
|
|
})
|
|
});
|
|
|
|
let mut start_location = source_code.line_column(message.expect_range().start());
|
|
let mut end_location = source_code.line_column(message.expect_range().end());
|
|
let mut noqa_location = message
|
|
.noqa_offset()
|
|
.map(|offset| source_code.line_column(offset));
|
|
let mut notebook_cell_index = None;
|
|
|
|
if let Some(notebook_index) = notebook_index {
|
|
notebook_cell_index = Some(
|
|
notebook_index
|
|
.cell(start_location.line)
|
|
.unwrap_or(OneIndexed::MIN),
|
|
);
|
|
start_location = notebook_index.translate_line_column(&start_location);
|
|
end_location = notebook_index.translate_line_column(&end_location);
|
|
noqa_location =
|
|
noqa_location.map(|location| notebook_index.translate_line_column(&location));
|
|
}
|
|
|
|
json!({
|
|
"code": message.secondary_code(),
|
|
"url": message.to_url(),
|
|
"message": message.body(),
|
|
"fix": fix,
|
|
"cell": notebook_cell_index,
|
|
"location": location_to_json(start_location),
|
|
"end_location": location_to_json(end_location),
|
|
"filename": filename,
|
|
"noqa_row": noqa_location.map(|location| location.line)
|
|
})
|
|
}
|
|
|
|
fn location_to_json(location: LineColumn) -> serde_json::Value {
|
|
json!({
|
|
"row": location.line,
|
|
"column": location.column
|
|
})
|
|
}
|
|
|
|
struct ExpandedEdits<'a> {
|
|
edits: &'a [Edit],
|
|
source_code: &'a SourceCode<'a, 'a>,
|
|
notebook_index: Option<&'a NotebookIndex>,
|
|
}
|
|
|
|
impl Serialize for ExpandedEdits<'_> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let mut s = serializer.serialize_seq(Some(self.edits.len()))?;
|
|
|
|
for edit in self.edits {
|
|
let mut location = self.source_code.line_column(edit.start());
|
|
let mut end_location = self.source_code.line_column(edit.end());
|
|
|
|
if let Some(notebook_index) = self.notebook_index {
|
|
// There exists a newline between each cell's source code in the
|
|
// concatenated source code in Ruff. This newline doesn't actually
|
|
// exists in the JSON source field.
|
|
//
|
|
// Now, certain edits may try to remove this newline, which means
|
|
// the edit will spill over to the first character of the next cell.
|
|
// If it does, we need to translate the end location to the last
|
|
// character of the previous cell.
|
|
match (
|
|
notebook_index.cell(location.line),
|
|
notebook_index.cell(end_location.line),
|
|
) {
|
|
(Some(start_cell), Some(end_cell)) if start_cell != end_cell => {
|
|
debug_assert_eq!(end_location.column.get(), 1);
|
|
|
|
let prev_row = end_location.line.saturating_sub(1);
|
|
end_location = LineColumn {
|
|
line: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN),
|
|
column: self
|
|
.source_code
|
|
.line_column(self.source_code.line_end_exclusive(prev_row))
|
|
.column,
|
|
};
|
|
}
|
|
(Some(_), None) => {
|
|
debug_assert_eq!(end_location.column.get(), 1);
|
|
|
|
let prev_row = end_location.line.saturating_sub(1);
|
|
end_location = LineColumn {
|
|
line: notebook_index.cell_row(prev_row).unwrap_or(OneIndexed::MIN),
|
|
column: self
|
|
.source_code
|
|
.line_column(self.source_code.line_end_exclusive(prev_row))
|
|
.column,
|
|
};
|
|
}
|
|
_ => {
|
|
end_location = notebook_index.translate_line_column(&end_location);
|
|
}
|
|
}
|
|
location = notebook_index.translate_line_column(&location);
|
|
}
|
|
|
|
let value = json!({
|
|
"content": edit.content().unwrap_or_default(),
|
|
"location": location_to_json(location),
|
|
"end_location": location_to_json(end_location)
|
|
});
|
|
|
|
s.serialize_element(&value)?;
|
|
}
|
|
|
|
s.end()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use insta::assert_snapshot;
|
|
|
|
use crate::message::JsonEmitter;
|
|
use crate::message::tests::{
|
|
capture_emitter_notebook_output, capture_emitter_output, create_diagnostics,
|
|
create_notebook_diagnostics, create_syntax_error_diagnostics,
|
|
};
|
|
|
|
#[test]
|
|
fn output() {
|
|
let mut emitter = JsonEmitter;
|
|
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
|
|
|
|
assert_snapshot!(content);
|
|
}
|
|
|
|
#[test]
|
|
fn syntax_errors() {
|
|
let mut emitter = JsonEmitter;
|
|
let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics());
|
|
|
|
assert_snapshot!(content);
|
|
}
|
|
|
|
#[test]
|
|
fn notebook_output() {
|
|
let mut emitter = JsonEmitter;
|
|
let (diagnostics, notebook_indexes) = create_notebook_diagnostics();
|
|
let content =
|
|
capture_emitter_notebook_output(&mut emitter, &diagnostics, ¬ebook_indexes);
|
|
|
|
assert_snapshot!(content);
|
|
}
|
|
}
|