ruff/crates/ruff_linter/src/message/diff.rs
Brent Westbrook 981bd70d39
Convert Message::SyntaxError to use Diagnostic internally (#17784)
## Summary

This PR is a first step toward integration of the new `Diagnostic` type
into ruff. There are two main changes:
- A new `UnifiedFile` enum wrapping `File` for red-knot and a
`SourceFile` for ruff
- ruff's `Message::SyntaxError` variant is now a `Diagnostic` instead of
a `SyntaxErrorMessage`

The second of these changes was mostly just a proof of concept for the
first, and it went pretty smoothly. Converting `DiagnosticMessage`s will
be most of the work in replacing `Message` entirely.

## Test Plan

Existing tests, which show no changes.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-05-08 12:45:51 -04:00

203 lines
6.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::fmt::{Display, Formatter};
use std::num::NonZeroUsize;
use colored::{Color, ColoredString, Colorize, Styles};
use ruff_text_size::{Ranged, TextRange, TextSize};
use similar::{ChangeTag, TextDiff};
use ruff_diagnostics::{Applicability, Fix};
use ruff_source_file::{OneIndexed, SourceFile};
use crate::message::Message;
use crate::text_helpers::ShowNonprinting;
/// 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: SourceFile,
}
impl<'a> Diff<'a> {
pub(crate) fn from_message(message: &'a Message) -> Option<Diff<'a>> {
message.fix().map(|fix| Diff {
source_code: message.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
}