Skip to content

Commit 0fcb000

Browse files
committed
extract each lint into its own module
1 parent 6c93427 commit 0fcb000

File tree

5 files changed

+430
-391
lines changed

5 files changed

+430
-391
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::macros::MacroCall;
3+
use clippy_utils::source::expand_past_previous_comma;
4+
use clippy_utils::sym;
5+
use rustc_ast::{FormatArgs, FormatArgsPiece};
6+
use rustc_errors::Applicability;
7+
use rustc_lint::LateContext;
8+
9+
use super::{PRINTLN_EMPTY_STRING, WRITELN_EMPTY_STRING};
10+
11+
pub(super) fn check(cx: &LateContext<'_>, format_args: &FormatArgs, macro_call: &MacroCall, name: &str) {
12+
if let [FormatArgsPiece::Literal(sym::LF)] = &format_args.template[..] {
13+
let mut span = format_args.span;
14+
15+
let lint = if name == "writeln" {
16+
span = expand_past_previous_comma(cx, span);
17+
18+
WRITELN_EMPTY_STRING
19+
} else {
20+
PRINTLN_EMPTY_STRING
21+
};
22+
23+
span_lint_and_then(
24+
cx,
25+
lint,
26+
macro_call.span,
27+
format!("empty string literal in `{name}!`"),
28+
|diag| {
29+
diag.span_suggestion(
30+
span,
31+
"remove the empty string",
32+
String::new(),
33+
Applicability::MachineApplicable,
34+
);
35+
},
36+
);
37+
}
38+
}

clippy_lints/src/write/literal.rs

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::macros::format_arg_removal_span;
3+
use clippy_utils::source::SpanRangeExt;
4+
use clippy_utils::sym;
5+
use rustc_ast::token::LitKind;
6+
use rustc_ast::{
7+
FormatArgPosition, FormatArgPositionKind, FormatArgs, FormatArgsPiece, FormatCount, FormatOptions,
8+
FormatPlaceholder, FormatTrait,
9+
};
10+
use rustc_errors::Applicability;
11+
use rustc_lint::LateContext;
12+
use rustc_span::Span;
13+
14+
use super::{PRINT_LITERAL, WRITE_LITERAL};
15+
16+
pub(super) fn check(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) {
17+
let arg_index = |argument: &FormatArgPosition| argument.index.unwrap_or_else(|pos| pos);
18+
19+
let lint_name = if name.starts_with("write") {
20+
WRITE_LITERAL
21+
} else {
22+
PRINT_LITERAL
23+
};
24+
25+
let mut counts = vec![0u32; format_args.arguments.all_args().len()];
26+
for piece in &format_args.template {
27+
if let FormatArgsPiece::Placeholder(placeholder) = piece {
28+
counts[arg_index(&placeholder.argument)] += 1;
29+
}
30+
}
31+
32+
let mut suggestion: Vec<(Span, String)> = vec![];
33+
// holds index of replaced positional arguments; used to decrement the index of the remaining
34+
// positional arguments.
35+
let mut replaced_position: Vec<usize> = vec![];
36+
let mut sug_span: Option<Span> = None;
37+
38+
for piece in &format_args.template {
39+
if let FormatArgsPiece::Placeholder(FormatPlaceholder {
40+
argument,
41+
span: Some(placeholder_span),
42+
format_trait: FormatTrait::Display,
43+
format_options,
44+
}) = piece
45+
&& *format_options == FormatOptions::default()
46+
&& let index = arg_index(argument)
47+
&& counts[index] == 1
48+
&& let Some(arg) = format_args.arguments.by_index(index)
49+
&& let rustc_ast::ExprKind::Lit(lit) = &arg.expr.kind
50+
&& !arg.expr.span.from_expansion()
51+
&& let Some(value_string) = arg.expr.span.get_source_text(cx)
52+
{
53+
let (replacement, replace_raw) = match lit.kind {
54+
LitKind::Str | LitKind::StrRaw(_) => match extract_str_literal(&value_string) {
55+
Some(extracted) => extracted,
56+
None => return,
57+
},
58+
LitKind::Char => (
59+
match lit.symbol {
60+
sym::DOUBLE_QUOTE => "\\\"",
61+
sym::BACKSLASH_SINGLE_QUOTE => "'",
62+
_ => match value_string.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
63+
Some(stripped) => stripped,
64+
None => return,
65+
},
66+
}
67+
.to_string(),
68+
false,
69+
),
70+
LitKind::Bool => (lit.symbol.to_string(), false),
71+
_ => continue,
72+
};
73+
74+
let Some(format_string_snippet) = format_args.span.get_source_text(cx) else {
75+
continue;
76+
};
77+
let format_string_is_raw = format_string_snippet.starts_with('r');
78+
79+
let replacement = match (format_string_is_raw, replace_raw) {
80+
(false, false) => Some(replacement),
81+
(false, true) => Some(replacement.replace('\\', "\\\\").replace('"', "\\\"")),
82+
(true, false) => match conservative_unescape(&replacement) {
83+
Ok(unescaped) => Some(unescaped),
84+
Err(UnescapeErr::Lint) => None,
85+
Err(UnescapeErr::Ignore) => continue,
86+
},
87+
(true, true) => {
88+
if replacement.contains(['#', '"']) {
89+
None
90+
} else {
91+
Some(replacement)
92+
}
93+
},
94+
};
95+
96+
sug_span = Some(sug_span.unwrap_or(arg.expr.span).to(arg.expr.span));
97+
98+
if let Some((_, index)) = format_arg_piece_span(piece) {
99+
replaced_position.push(index);
100+
}
101+
102+
if let Some(replacement) = replacement
103+
// `format!("{}", "a")`, `format!("{named}", named = "b")
104+
// ~~~~~ ~~~~~~~~~~~~~
105+
&& let Some(removal_span) = format_arg_removal_span(format_args, index)
106+
{
107+
let replacement = escape_braces(&replacement, !format_string_is_raw && !replace_raw);
108+
suggestion.push((*placeholder_span, replacement));
109+
suggestion.push((removal_span, String::new()));
110+
}
111+
}
112+
}
113+
114+
// Decrement the index of the remaining by the number of replaced positional arguments
115+
if !suggestion.is_empty() {
116+
for piece in &format_args.template {
117+
relocalize_format_args_indexes(piece, &mut suggestion, &replaced_position);
118+
}
119+
}
120+
121+
if let Some(span) = sug_span {
122+
span_lint_and_then(cx, lint_name, span, "literal with an empty format string", |diag| {
123+
if !suggestion.is_empty() {
124+
diag.multipart_suggestion("try", suggestion, Applicability::MachineApplicable);
125+
}
126+
});
127+
}
128+
}
129+
130+
/// Extract Span and its index from the given `piece`
131+
fn format_arg_piece_span(piece: &FormatArgsPiece) -> Option<(Span, usize)> {
132+
match piece {
133+
FormatArgsPiece::Placeholder(FormatPlaceholder {
134+
argument: FormatArgPosition { index: Ok(index), .. },
135+
span: Some(span),
136+
..
137+
}) => Some((*span, *index)),
138+
_ => None,
139+
}
140+
}
141+
142+
/// Relocalizes the indexes of positional arguments in the format string
143+
fn relocalize_format_args_indexes(
144+
piece: &FormatArgsPiece,
145+
suggestion: &mut Vec<(Span, String)>,
146+
replaced_position: &[usize],
147+
) {
148+
if let FormatArgsPiece::Placeholder(FormatPlaceholder {
149+
argument:
150+
FormatArgPosition {
151+
index: Ok(index),
152+
// Only consider positional arguments
153+
kind: FormatArgPositionKind::Number,
154+
span: Some(span),
155+
},
156+
format_options,
157+
..
158+
}) = piece
159+
{
160+
if suggestion.iter().any(|(s, _)| s.overlaps(*span)) {
161+
// If the span is already in the suggestion, we don't need to process it again
162+
return;
163+
}
164+
165+
// lambda to get the decremented index based on the replaced positions
166+
let decremented_index = |index: usize| -> usize {
167+
let decrement = replaced_position.iter().filter(|&&i| i < index).count();
168+
index - decrement
169+
};
170+
171+
suggestion.push((*span, decremented_index(*index).to_string()));
172+
173+
// If there are format options, we need to handle them as well
174+
if *format_options != FormatOptions::default() {
175+
// lambda to process width and precision format counts and add them to the suggestion
176+
let mut process_format_count = |count: &Option<FormatCount>, formatter: &dyn Fn(usize) -> String| {
177+
if let Some(FormatCount::Argument(FormatArgPosition {
178+
index: Ok(format_arg_index),
179+
kind: FormatArgPositionKind::Number,
180+
span: Some(format_arg_span),
181+
})) = count
182+
{
183+
suggestion.push((*format_arg_span, formatter(decremented_index(*format_arg_index))));
184+
}
185+
};
186+
187+
process_format_count(&format_options.width, &|index: usize| format!("{index}$"));
188+
process_format_count(&format_options.precision, &|index: usize| format!(".{index}$"));
189+
}
190+
}
191+
}
192+
193+
/// Removes the raw marker, `#`s and quotes from a str, and returns if the literal is raw
194+
///
195+
/// `r#"a"#` -> (`a`, true)
196+
///
197+
/// `"b"` -> (`b`, false)
198+
fn extract_str_literal(literal: &str) -> Option<(String, bool)> {
199+
let (literal, raw) = match literal.strip_prefix('r') {
200+
Some(stripped) => (stripped.trim_matches('#'), true),
201+
None => (literal, false),
202+
};
203+
204+
Some((literal.strip_prefix('"')?.strip_suffix('"')?.to_string(), raw))
205+
}
206+
207+
enum UnescapeErr {
208+
/// Should still be linted, can be manually resolved by author, e.g.
209+
///
210+
/// ```ignore
211+
/// print!(r"{}", '"');
212+
/// ```
213+
Lint,
214+
/// Should not be linted, e.g.
215+
///
216+
/// ```ignore
217+
/// print!(r"{}", '\r');
218+
/// ```
219+
Ignore,
220+
}
221+
222+
/// Unescape a normal string into a raw string
223+
fn conservative_unescape(literal: &str) -> Result<String, UnescapeErr> {
224+
let mut unescaped = String::with_capacity(literal.len());
225+
let mut chars = literal.chars();
226+
let mut err = false;
227+
228+
while let Some(ch) = chars.next() {
229+
match ch {
230+
'#' => err = true,
231+
'\\' => match chars.next() {
232+
Some('\\') => unescaped.push('\\'),
233+
Some('"') => err = true,
234+
_ => return Err(UnescapeErr::Ignore),
235+
},
236+
_ => unescaped.push(ch),
237+
}
238+
}
239+
240+
if err { Err(UnescapeErr::Lint) } else { Ok(unescaped) }
241+
}
242+
243+
/// Replaces `{` with `{{` and `}` with `}}`. If `preserve_unicode_escapes` is `true` the braces
244+
/// in `\u{xxxx}` are left unmodified
245+
#[expect(clippy::match_same_arms)]
246+
fn escape_braces(literal: &str, preserve_unicode_escapes: bool) -> String {
247+
#[derive(Clone, Copy)]
248+
enum State {
249+
Normal,
250+
Backslash,
251+
UnicodeEscape,
252+
}
253+
254+
let mut escaped = String::with_capacity(literal.len());
255+
let mut state = State::Normal;
256+
257+
for ch in literal.chars() {
258+
state = match (ch, state) {
259+
// Escape braces outside of unicode escapes by doubling them up
260+
('{' | '}', State::Normal) => {
261+
escaped.push(ch);
262+
State::Normal
263+
},
264+
// If `preserve_unicode_escapes` isn't enabled stay in `State::Normal`, otherwise:
265+
//
266+
// \u{aaaa} \\ \x01
267+
// ^ ^ ^
268+
('\\', State::Normal) if preserve_unicode_escapes => State::Backslash,
269+
// \u{aaaa}
270+
// ^
271+
('u', State::Backslash) => State::UnicodeEscape,
272+
// \xAA \\
273+
// ^ ^
274+
(_, State::Backslash) => State::Normal,
275+
// \u{aaaa}
276+
// ^
277+
('}', State::UnicodeEscape) => State::Normal,
278+
_ => state,
279+
};
280+
281+
escaped.push(ch);
282+
}
283+
284+
escaped
285+
}

0 commit comments

Comments
 (0)