mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 21:05:08 +00:00

## Summary This PR updates the way syntax errors are handled throughout the linter. The main change is that it's now not considered as a rule which involves the following changes: * Update `Message` to be an enum with two variants - one for diagnostic message and the other for syntax error message * Provide methods on the new message enum to query information required by downstream usages This means that the syntax errors cannot be hidden / disabled via any disablement methods. These are: 1. Configuration via `select`, `ignore`, `per-file-ignores`, and their `extend-*` variants ```console $ cargo run -- check ~/playground/ruff/src/lsp.py --extend-select=E999 --no-preview --no-cache Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s Running `target/debug/ruff check /Users/dhruv/playground/ruff/src/lsp.py --extend-select=E999 --no-preview --no-cache` warning: Rule `E999` is deprecated and will be removed in a future release. Syntax errors will always be shown regardless of whether this rule is selected or not. /Users/dhruv/playground/ruff/src/lsp.py:1:8: F401 [*] `abc` imported but unused | 1 | import abc | ^^^ F401 2 | from pathlib import Path 3 | import os | = help: Remove unused import: `abc` ``` 3. Command-line flags via `--select`, `--ignore`, `--per-file-ignores`, and their `--extend-*` variants ```console $ cargo run -- check ~/playground/ruff/src/lsp.py --no-cache --config=~/playground/ruff/pyproject.toml Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s Running `target/debug/ruff check /Users/dhruv/playground/ruff/src/lsp.py --no-cache --config=/Users/dhruv/playground/ruff/pyproject.toml` warning: Rule `E999` is deprecated and will be removed in a future release. Syntax errors will always be shown regardless of whether this rule is selected or not. /Users/dhruv/playground/ruff/src/lsp.py:1:8: F401 [*] `abc` imported but unused | 1 | import abc | ^^^ F401 2 | from pathlib import Path 3 | import os | = help: Remove unused import: `abc` ``` This also means that the **output format** needs to be updated: 1. The `code`, `noqa_row`, `url` fields in the JSON output is optional (`null` for syntax errors) 2. Other formats are changed accordingly For each format, a new test case specific to syntax errors have been added. Please refer to the snapshot output for the exact format for syntax error message. The output of the `--statistics` flag will have a blank entry for syntax errors: ``` 315 F821 [ ] undefined-name 119 [ ] syntax-error 103 F811 [ ] redefined-while-unused ``` The **language server** is updated to consider the syntax errors by convert them into LSP diagnostic format separately. ### Preview There are no quick fixes provided to disable syntax errors. This will automatically work for `ruff-lsp` because the `noqa_row` field will be `null` in that case. <img width="772" alt="Screenshot 2024-06-26 at 14 57 08" src="aaac827e
-4777-4ac8-8c68-eaf9f2c36774"> Even with `noqa` comment, the syntax error is displayed: <img width="763" alt="Screenshot 2024-06-26 at 14 59 51" src="ba1afb68
-7eaf-4b44-91af-6d93246475e2"> Rule documentation page: <img width="1371" alt="Screenshot 2024-06-26 at 16 48 07" src="524f01df
-d91f-4ac0-86cc-40e76b318b24"> ## Test Plan - [x] Disablement methods via config shows a warning - [x] `select`, `extend-select` - [ ] ~`ignore`~ _doesn't show any message_ - [ ] ~`per-file-ignores`, `extend-per-file-ignores`~ _doesn't show any message_ - [x] Disablement methods via command-line flag shows a warning - [x] `--select`, `--extend-select` - [ ] ~`--ignore`~ _doesn't show any message_ - [ ] ~`--per-file-ignores`, `--extend-per-file-ignores`~ _doesn't show any message_ - [x] File with syntax errors should exit with code 1 - [x] Language server - [x] Should show diagnostics for syntax errors - [x] Should not recommend a quick fix edit for adding `noqa` comment - [x] Same for `ruff-lsp` resolves: #8447
203 lines
6.5 KiB
Rust
203 lines
6.5 KiB
Rust
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: &'a SourceFile,
|
||
}
|
||
|
||
impl<'a> Diff<'a> {
|
||
pub(crate) fn from_message(message: &'a Message) -> Option<Diff> {
|
||
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
|
||
}
|