Truncate some messages in diagnostics (#6748)

## Summary

I noticed this in the ecosystem CI check from
https://github.com/astral-sh/ruff/pull/6742. If we include source code
directly in a diagnostic, we need to be careful to avoid rendering
multi-line diagnostics or even excessively long diagnostics.

## Test Plan

`cargo test`
This commit is contained in:
Charlie Marsh 2023-08-21 23:46:24 -04:00 committed by GitHub
parent 0aad0c41f6
commit fa32cd9b6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 118 additions and 57 deletions

View File

@ -13,6 +13,7 @@ use crate::registry::{AsRule, Rule};
pub(crate) mod codemods; pub(crate) mod codemods;
pub(crate) mod edits; pub(crate) mod edits;
pub(crate) mod snippet;
pub(crate) mod source_map; pub(crate) mod source_map;
pub(crate) struct FixResult { pub(crate) struct FixResult {

View File

@ -0,0 +1,36 @@
use unicode_width::UnicodeWidthStr;
/// A snippet of source code for user-facing display, as in a diagnostic.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SourceCodeSnippet(String);
impl SourceCodeSnippet {
pub(crate) fn new(source_code: String) -> Self {
Self(source_code)
}
/// Return the full snippet for user-facing display, or `None` if the snippet should be
/// truncated.
pub(crate) fn full_display(&self) -> Option<&str> {
if Self::should_truncate(&self.0) {
None
} else {
Some(&self.0)
}
}
/// Return a truncated snippet for user-facing display.
pub(crate) fn truncated_display(&self) -> &str {
if Self::should_truncate(&self.0) {
"..."
} else {
&self.0
}
}
/// Returns `true` if the source code should be truncated when included in a user-facing
/// diagnostic.
fn should_truncate(source_code: &str) -> bool {
source_code.width() > 50 || source_code.contains(['\r', '\n'])
}
}

View File

@ -1,14 +1,15 @@
use anyhow::Result; use anyhow::Result;
use libcst_native::CompOp; use libcst_native::CompOp;
use ruff_python_ast::{self as ast, CmpOp, Expr, Ranged, UnaryOp};
use crate::autofix::codemods::CodegenStylist;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, CmpOp, Expr, Ranged, UnaryOp};
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_stdlib::str::{self}; use ruff_python_stdlib::str::{self};
use ruff_source_file::Locator; use ruff_source_file::Locator;
use crate::autofix::codemods::CodegenStylist;
use crate::autofix::snippet::SourceCodeSnippet;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::cst::matchers::{match_comparison, match_expression}; use crate::cst::matchers::{match_comparison, match_expression};
use crate::registry::AsRule; use crate::registry::AsRule;
@ -45,7 +46,7 @@ use crate::registry::AsRule;
/// - [Python documentation: Assignment statements](https://docs.python.org/3/reference/simple_stmts.html#assignment-statements) /// - [Python documentation: Assignment statements](https://docs.python.org/3/reference/simple_stmts.html#assignment-statements)
#[violation] #[violation]
pub struct YodaConditions { pub struct YodaConditions {
pub suggestion: Option<String>, suggestion: Option<SourceCodeSnippet>,
} }
impl Violation for YodaConditions { impl Violation for YodaConditions {
@ -54,7 +55,10 @@ impl Violation for YodaConditions {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let YodaConditions { suggestion } = self; let YodaConditions { suggestion } = self;
if let Some(suggestion) = suggestion { if let Some(suggestion) = suggestion
.as_ref()
.and_then(SourceCodeSnippet::full_display)
{
format!("Yoda conditions are discouraged, use `{suggestion}` instead") format!("Yoda conditions are discouraged, use `{suggestion}` instead")
} else { } else {
format!("Yoda conditions are discouraged") format!("Yoda conditions are discouraged")
@ -63,9 +67,13 @@ impl Violation for YodaConditions {
fn autofix_title(&self) -> Option<String> { fn autofix_title(&self) -> Option<String> {
let YodaConditions { suggestion } = self; let YodaConditions { suggestion } = self;
suggestion suggestion.as_ref().map(|suggestion| {
.as_ref() if let Some(suggestion) = suggestion.full_display() {
.map(|suggestion| format!("Replace Yoda condition with `{suggestion}`")) format!("Replace Yoda condition with `{suggestion}`")
} else {
format!("Replace Yoda condition")
}
})
} }
} }
@ -178,7 +186,7 @@ pub(crate) fn yoda_conditions(
if let Ok(suggestion) = reverse_comparison(expr, checker.locator(), checker.stylist()) { if let Ok(suggestion) = reverse_comparison(expr, checker.locator(), checker.stylist()) {
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
YodaConditions { YodaConditions {
suggestion: Some(suggestion.to_string()), suggestion: Some(SourceCodeSnippet::new(suggestion.clone())),
}, },
expr.range(), expr.range(),
); );

View File

@ -290,7 +290,7 @@ SIM300.py:15:1: SIM300 [*] Yoda conditions are discouraged, use `(number - 100)
17 17 | 17 17 |
18 18 | # OK 18 18 | # OK
SIM300.py:16:1: SIM300 [*] Yoda conditions are discouraged, use `(60 * 60) < SomeClass().settings.SOME_CONSTANT_VALUE` instead SIM300.py:16:1: SIM300 [*] Yoda conditions are discouraged
| |
14 | JediOrder.YODA == age # SIM300 14 | JediOrder.YODA == age # SIM300
15 | 0 < (number - 100) # SIM300 15 | 0 < (number - 100) # SIM300
@ -299,7 +299,7 @@ SIM300.py:16:1: SIM300 [*] Yoda conditions are discouraged, use `(60 * 60) < Som
17 | 17 |
18 | # OK 18 | # OK
| |
= help: Replace Yoda condition with `(60 * 60) < SomeClass().settings.SOME_CONSTANT_VALUE` = help: Replace Yoda condition
Fix Fix
13 13 | YODA >= age # SIM300 13 13 | YODA >= age # SIM300

View File

@ -1,10 +1,11 @@
use itertools::Itertools; use itertools::Itertools;
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, Ranged};
use ruff_text_size::TextRange;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, Ranged};
use ruff_text_size::TextRange;
use crate::autofix::snippet::SourceCodeSnippet;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::registry::AsRule; use crate::registry::AsRule;
use crate::rules::flynt::helpers; use crate::rules::flynt::helpers;
@ -29,19 +30,27 @@ use crate::rules::flynt::helpers;
/// - [Python documentation: f-strings](https://docs.python.org/3/reference/lexical_analysis.html#f-strings) /// - [Python documentation: f-strings](https://docs.python.org/3/reference/lexical_analysis.html#f-strings)
#[violation] #[violation]
pub struct StaticJoinToFString { pub struct StaticJoinToFString {
expr: String, expression: SourceCodeSnippet,
} }
impl AlwaysAutofixableViolation for StaticJoinToFString { impl AlwaysAutofixableViolation for StaticJoinToFString {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let StaticJoinToFString { expr } = self; let StaticJoinToFString { expression } = self;
format!("Consider `{expr}` instead of string join") if let Some(expression) = expression.full_display() {
format!("Consider `{expression}` instead of string join")
} else {
format!("Consider f-string instead of string join")
}
} }
fn autofix_title(&self) -> String { fn autofix_title(&self) -> String {
let StaticJoinToFString { expr } = self; let StaticJoinToFString { expression } = self;
format!("Replace with `{expr}`") if let Some(expression) = expression.full_display() {
format!("Replace with `{expression}`")
} else {
format!("Replace with f-string")
}
} }
} }
@ -140,7 +149,7 @@ pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner:
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
StaticJoinToFString { StaticJoinToFString {
expr: contents.clone(), expression: SourceCodeSnippet::new(contents.clone()),
}, },
expr.range(), expr.range(),
); );

View File

@ -36,7 +36,8 @@ pub struct BadStringFormatCharacter {
impl Violation for BadStringFormatCharacter { impl Violation for BadStringFormatCharacter {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
format!("Unsupported format character '{}'", self.format_char) let BadStringFormatCharacter { format_char } = self;
format!("Unsupported format character '{format_char}'")
} }
} }

View File

@ -5,6 +5,7 @@ use itertools::{any, Itertools};
use ruff_python_ast::{BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare, Ranged}; use ruff_python_ast::{BoolOp, CmpOp, Expr, ExprBoolOp, ExprCompare, Ranged};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use crate::autofix::snippet::SourceCodeSnippet;
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::hashable::HashableExpr; use ruff_python_ast::hashable::HashableExpr;
@ -42,16 +43,22 @@ use crate::checkers::ast::Checker;
/// - [Python documentation: `set`](https://docs.python.org/3/library/stdtypes.html#set) /// - [Python documentation: `set`](https://docs.python.org/3/library/stdtypes.html#set)
#[violation] #[violation]
pub struct RepeatedEqualityComparisonTarget { pub struct RepeatedEqualityComparisonTarget {
expr: String, expression: SourceCodeSnippet,
} }
impl Violation for RepeatedEqualityComparisonTarget { impl Violation for RepeatedEqualityComparisonTarget {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let RepeatedEqualityComparisonTarget { expr } = self; let RepeatedEqualityComparisonTarget { expression } = self;
format!( if let Some(expression) = expression.full_display() {
"Consider merging multiple comparisons: `{expr}`. Use a `set` if the elements are hashable." format!(
) "Consider merging multiple comparisons: `{expression}`. Use a `set` if the elements are hashable."
)
} else {
format!(
"Consider merging multiple comparisons. Use a `set` if the elements are hashable."
)
}
} }
} }
@ -84,12 +91,12 @@ pub(crate) fn repeated_equality_comparison_target(checker: &mut Checker, bool_op
if count > 1 { if count > 1 {
checker.diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
RepeatedEqualityComparisonTarget { RepeatedEqualityComparisonTarget {
expr: merged_membership_test( expression: SourceCodeSnippet::new(merged_membership_test(
left.as_expr(), left.as_expr(),
bool_op.op, bool_op.op,
&comparators, &comparators,
checker.locator(), checker.locator(),
), )),
}, },
bool_op.range(), bool_op.range(),
)); ));

View File

@ -1,9 +1,9 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, ExprContext, Operator, Ranged}; use ruff_python_ast::{self as ast, Expr, ExprContext, Operator, Ranged};
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use crate::autofix::snippet::SourceCodeSnippet;
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::registry::AsRule; use crate::registry::AsRule;
@ -39,7 +39,7 @@ use crate::registry::AsRule;
/// - [Python documentation: Sequence Types — `list`, `tuple`, `range`](https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range) /// - [Python documentation: Sequence Types — `list`, `tuple`, `range`](https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range)
#[violation] #[violation]
pub struct CollectionLiteralConcatenation { pub struct CollectionLiteralConcatenation {
expr: String, expression: SourceCodeSnippet,
} }
impl Violation for CollectionLiteralConcatenation { impl Violation for CollectionLiteralConcatenation {
@ -47,13 +47,21 @@ impl Violation for CollectionLiteralConcatenation {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let CollectionLiteralConcatenation { expr } = self; let CollectionLiteralConcatenation { expression } = self;
format!("Consider `{expr}` instead of concatenation") if let Some(expression) = expression.full_display() {
format!("Consider `{expression}` instead of concatenation")
} else {
format!("Consider iterable unpacking instead of concatenation")
}
} }
fn autofix_title(&self) -> Option<String> { fn autofix_title(&self) -> Option<String> {
let CollectionLiteralConcatenation { expr } = self; let CollectionLiteralConcatenation { expression } = self;
Some(format!("Replace with `{expr}`")) if let Some(expression) = expression.full_display() {
Some(format!("Replace with `{expression}`"))
} else {
Some(format!("Replace with iterable unpacking"))
}
} }
} }
@ -186,7 +194,7 @@ pub(crate) fn collection_literal_concatenation(checker: &mut Checker, expr: &Exp
}; };
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
CollectionLiteralConcatenation { CollectionLiteralConcatenation {
expr: contents.clone(), expression: SourceCodeSnippet::new(contents.clone()),
}, },
expr.range(), expr.range(),
); );

View File

@ -90,7 +90,8 @@ impl Violation for ImplicitOptional {
} }
fn autofix_title(&self) -> Option<String> { fn autofix_title(&self) -> Option<String> {
Some(format!("Convert to `{}`", self.conversion_type)) let ImplicitOptional { conversion_type } = self;
Some(format!("Convert to `{conversion_type}`"))
} }
} }

View File

@ -40,6 +40,7 @@ impl Violation for InvalidPyprojectToml {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
format!("Failed to parse pyproject.toml: {}", self.message) let InvalidPyprojectToml { message } = self;
format!("Failed to parse pyproject.toml: {message}")
} }
} }

View File

@ -1,7 +1,6 @@
use std::borrow::Cow; use std::borrow::Cow;
use num_traits::Zero; use num_traits::Zero;
use unicode_width::UnicodeWidthStr;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
@ -9,6 +8,7 @@ use ruff_python_ast::{self as ast, Arguments, Comprehension, Constant, Expr, Ran
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use crate::autofix::snippet::SourceCodeSnippet;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::registry::AsRule; use crate::registry::AsRule;
@ -47,35 +47,24 @@ use crate::registry::AsRule;
/// - [Iterators and Iterables in Python: Run Efficient Iterations](https://realpython.com/python-iterators-iterables/#when-to-use-an-iterator-in-python) /// - [Iterators and Iterables in Python: Run Efficient Iterations](https://realpython.com/python-iterators-iterables/#when-to-use-an-iterator-in-python)
#[violation] #[violation]
pub(crate) struct UnnecessaryIterableAllocationForFirstElement { pub(crate) struct UnnecessaryIterableAllocationForFirstElement {
iterable: String, iterable: SourceCodeSnippet,
} }
impl AlwaysAutofixableViolation for UnnecessaryIterableAllocationForFirstElement { impl AlwaysAutofixableViolation for UnnecessaryIterableAllocationForFirstElement {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
let UnnecessaryIterableAllocationForFirstElement { iterable } = self; let UnnecessaryIterableAllocationForFirstElement { iterable } = self;
let iterable = Self::truncate(iterable); let iterable = iterable.truncated_display();
format!("Prefer `next({iterable})` over single element slice") format!("Prefer `next({iterable})` over single element slice")
} }
fn autofix_title(&self) -> String { fn autofix_title(&self) -> String {
let UnnecessaryIterableAllocationForFirstElement { iterable } = self; let UnnecessaryIterableAllocationForFirstElement { iterable } = self;
let iterable = Self::truncate(iterable); let iterable = iterable.truncated_display();
format!("Replace with `next({iterable})`") format!("Replace with `next({iterable})`")
} }
} }
impl UnnecessaryIterableAllocationForFirstElement {
/// If the iterable is too long, or spans multiple lines, truncate it.
fn truncate(iterable: &str) -> &str {
if iterable.width() > 40 || iterable.contains(['\r', '\n']) {
"..."
} else {
iterable
}
}
}
/// RUF015 /// RUF015
pub(crate) fn unnecessary_iterable_allocation_for_first_element( pub(crate) fn unnecessary_iterable_allocation_for_first_element(
checker: &mut Checker, checker: &mut Checker,
@ -104,7 +93,7 @@ pub(crate) fn unnecessary_iterable_allocation_for_first_element(
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
UnnecessaryIterableAllocationForFirstElement { UnnecessaryIterableAllocationForFirstElement {
iterable: iterable.to_string(), iterable: SourceCodeSnippet::new(iterable.to_string()),
}, },
*range, *range,
); );

View File

@ -187,7 +187,7 @@ RUF005.py:47:16: RUF005 [*] Consider `("we all feel", *Fun.words)` instead of co
49 49 | chain = ["a", "b", "c"] + eggs + list(("yes", "no", "pants") + zoob) 49 49 | chain = ["a", "b", "c"] + eggs + list(("yes", "no", "pants") + zoob)
50 50 | 50 50 |
RUF005.py:49:9: RUF005 [*] Consider `["a", "b", "c", *eggs, *list(("yes", "no", "pants") + zoob)]` instead of concatenation RUF005.py:49:9: RUF005 [*] Consider iterable unpacking instead of concatenation
| |
47 | astonishment = ("we all feel",) + Fun.words 47 | astonishment = ("we all feel",) + Fun.words
48 | 48 |
@ -196,7 +196,7 @@ RUF005.py:49:9: RUF005 [*] Consider `["a", "b", "c", *eggs, *list(("yes", "no",
50 | 50 |
51 | baz = () + zoob 51 | baz = () + zoob
| |
= help: Replace with `["a", "b", "c", *eggs, *list(("yes", "no", "pants") + zoob)]` = help: Replace with iterable unpacking
Suggested fix Suggested fix
46 46 | excitement = ("we all think",) + Fun().yay() 46 46 | excitement = ("we all think",) + Fun().yay()
@ -294,14 +294,14 @@ RUF005.py:56:15: RUF005 [*] Consider `[sys.executable, "-m", "pylint", *args, pa
58 58 | b = a + [2, 3] + [4] 58 58 | b = a + [2, 3] + [4]
59 59 | 59 59 |
RUF005.py:57:21: RUF005 [*] Consider `(sys.executable, "-m", "pylint", *args, path, path2)` instead of concatenation RUF005.py:57:21: RUF005 [*] Consider iterable unpacking instead of concatenation
| |
56 | pylint_call = [sys.executable, "-m", "pylint"] + args + [path] 56 | pylint_call = [sys.executable, "-m", "pylint"] + args + [path]
57 | pylint_call_tuple = (sys.executable, "-m", "pylint") + args + (path, path2) 57 | pylint_call_tuple = (sys.executable, "-m", "pylint") + args + (path, path2)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF005 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF005
58 | b = a + [2, 3] + [4] 58 | b = a + [2, 3] + [4]
| |
= help: Replace with `(sys.executable, "-m", "pylint", *args, path, path2)` = help: Replace with iterable unpacking
Suggested fix Suggested fix
54 54 | ] 54 54 | ]