mirror of
https://github.com/astral-sh/ruff.git
synced 2025-12-07 19:06:47 +00:00
## Summary See https://github.com/astral-sh/ruff/pull/19133#discussion_r2198413586 for recent discussion. This PR moves to using structs for the types in our JSON output format instead of the `json!` macro. I didn't rename any of the `message` references because that should be handled when rebasing #19133 onto this. My plan for handling the `preview` behavior with the new diagnostics is to use a wrapper enum. Something like: ```rust #[derive(Serialize)] #[serde(untagged)] pub(crate) enum JsonDiagnostic<'a> { Old(OldJsonDiagnostic<'a>), } #[derive(Serialize)] pub(crate) struct OldJsonDiagnostic<'a> { // ... } ``` Initially I thought I could use a `&dyn Serialize` for the affected fields, but I see that `Serialize` isn't dyn-compatible in testing this now. ## Test Plan Existing tests. One quirk of the new types is that their fields are in alphabetical order. I guess `json!` sorts the fields alphabetically? The tests were failing before I sorted the struct fields. ## Other formats It looks like the `rdjson`, `sarif`, and `gitlab` formats also use `json!`, so if we decide to merge this, I can do something similar for those before moving them to the new diagnostic format.
202 lines
6.5 KiB
Rust
202 lines
6.5 KiB
Rust
use std::fmt::{Display, Formatter};
|
||
use std::num::NonZeroUsize;
|
||
|
||
use colored::{Color, ColoredString, Colorize, Styles};
|
||
use similar::{ChangeTag, TextDiff};
|
||
|
||
use ruff_db::diagnostic::Diagnostic;
|
||
use ruff_source_file::{OneIndexed, SourceFile};
|
||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||
|
||
use crate::text_helpers::ShowNonprinting;
|
||
use crate::{Applicability, Fix};
|
||
|
||
/// Renders a diff that shows the code fixes.
|
||
///
|
||
/// The implementation isn't fully fledged out and only used by tests. Before using in production, try
|
||
/// * Improve layout
|
||
/// * Replace tabs with spaces for a consistent experience across terminals
|
||
/// * Replace zero-width whitespaces
|
||
/// * Print a simpler diff if only a single line has changed
|
||
/// * Compute the diff from the [`Edit`] because diff calculation is expensive.
|
||
pub(super) struct Diff<'a> {
|
||
fix: &'a Fix,
|
||
source_code: &'a SourceFile,
|
||
}
|
||
|
||
impl<'a> Diff<'a> {
|
||
pub(crate) fn from_message(message: &'a Diagnostic) -> Option<Diff<'a>> {
|
||
message.fix().map(|fix| Diff {
|
||
source_code: message.expect_ruff_source_file(),
|
||
fix,
|
||
})
|
||
}
|
||
}
|
||
|
||
impl Display for Diff<'_> {
|
||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||
// TODO(dhruvmanila): Add support for Notebook cells once it's user-facing
|
||
let mut output = String::with_capacity(self.source_code.source_text().len());
|
||
let mut last_end = TextSize::default();
|
||
|
||
for edit in self.fix.edits() {
|
||
output.push_str(
|
||
self.source_code
|
||
.slice(TextRange::new(last_end, edit.start())),
|
||
);
|
||
output.push_str(edit.content().unwrap_or_default());
|
||
last_end = edit.end();
|
||
}
|
||
|
||
output.push_str(&self.source_code.source_text()[usize::from(last_end)..]);
|
||
|
||
let diff = TextDiff::from_lines(self.source_code.source_text(), &output);
|
||
|
||
let message = match self.fix.applicability() {
|
||
// TODO(zanieb): Adjust this messaging once it's user-facing
|
||
Applicability::Safe => "Safe fix",
|
||
Applicability::Unsafe => "Unsafe fix",
|
||
Applicability::DisplayOnly => "Display-only fix",
|
||
};
|
||
writeln!(f, "ℹ {}", message.blue())?;
|
||
|
||
let (largest_old, largest_new) = diff
|
||
.ops()
|
||
.last()
|
||
.map(|op| (op.old_range().start, op.new_range().start))
|
||
.unwrap_or_default();
|
||
|
||
let digit_with =
|
||
calculate_print_width(OneIndexed::from_zero_indexed(largest_new.max(largest_old)));
|
||
|
||
for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
|
||
if idx > 0 {
|
||
writeln!(f, "{:-^1$}", "-", 80)?;
|
||
}
|
||
for op in group {
|
||
for change in diff.iter_inline_changes(op) {
|
||
let sign = match change.tag() {
|
||
ChangeTag::Delete => "-",
|
||
ChangeTag::Insert => "+",
|
||
ChangeTag::Equal => " ",
|
||
};
|
||
|
||
let line_style = LineStyle::from(change.tag());
|
||
|
||
let old_index = change.old_index().map(OneIndexed::from_zero_indexed);
|
||
let new_index = change.new_index().map(OneIndexed::from_zero_indexed);
|
||
|
||
write!(
|
||
f,
|
||
"{} {} |{}",
|
||
Line {
|
||
index: old_index,
|
||
width: digit_with
|
||
},
|
||
Line {
|
||
index: new_index,
|
||
width: digit_with
|
||
},
|
||
line_style.apply_to(sign).bold()
|
||
)?;
|
||
|
||
for (emphasized, value) in change.iter_strings_lossy() {
|
||
let value = value.show_nonprinting();
|
||
if emphasized {
|
||
write!(f, "{}", line_style.apply_to(&value).underline().on_black())?;
|
||
} else {
|
||
write!(f, "{}", line_style.apply_to(&value))?;
|
||
}
|
||
}
|
||
if change.missing_newline() {
|
||
writeln!(f)?;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
struct LineStyle {
|
||
fgcolor: Option<Color>,
|
||
style: Option<Styles>,
|
||
}
|
||
|
||
impl LineStyle {
|
||
fn apply_to(&self, input: &str) -> ColoredString {
|
||
let mut colored = ColoredString::from(input);
|
||
if let Some(color) = self.fgcolor {
|
||
colored = colored.color(color);
|
||
}
|
||
|
||
if let Some(style) = self.style {
|
||
match style {
|
||
Styles::Clear => colored.clear(),
|
||
Styles::Bold => colored.bold(),
|
||
Styles::Dimmed => colored.dimmed(),
|
||
Styles::Underline => colored.underline(),
|
||
Styles::Reversed => colored.reversed(),
|
||
Styles::Italic => colored.italic(),
|
||
Styles::Blink => colored.blink(),
|
||
Styles::Hidden => colored.hidden(),
|
||
Styles::Strikethrough => colored.strikethrough(),
|
||
}
|
||
} else {
|
||
colored
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<ChangeTag> for LineStyle {
|
||
fn from(value: ChangeTag) -> Self {
|
||
match value {
|
||
ChangeTag::Equal => LineStyle {
|
||
fgcolor: None,
|
||
style: Some(Styles::Dimmed),
|
||
},
|
||
ChangeTag::Delete => LineStyle {
|
||
fgcolor: Some(Color::Red),
|
||
style: None,
|
||
},
|
||
ChangeTag::Insert => LineStyle {
|
||
fgcolor: Some(Color::Green),
|
||
style: None,
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
struct Line {
|
||
index: Option<OneIndexed>,
|
||
width: NonZeroUsize,
|
||
}
|
||
|
||
impl Display for Line {
|
||
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
|
||
match self.index {
|
||
None => {
|
||
for _ in 0..self.width.get() {
|
||
f.write_str(" ")?;
|
||
}
|
||
Ok(())
|
||
}
|
||
Some(idx) => write!(f, "{:<width$}", idx, width = self.width.get()),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Calculate the length of the string representation of `value`
|
||
pub(super) fn calculate_print_width(mut value: OneIndexed) -> NonZeroUsize {
|
||
const TEN: OneIndexed = OneIndexed::from_zero_indexed(9);
|
||
|
||
let mut width = OneIndexed::ONE;
|
||
|
||
while value >= TEN {
|
||
value = OneIndexed::new(value.get() / 10).unwrap_or(OneIndexed::MIN);
|
||
width = width.checked_add(1).unwrap();
|
||
}
|
||
|
||
width
|
||
}
|