Skip to content

Commit 5be4e71

Browse files
committed
Auto merge of #4136 - euclio:println-writeln-suggestions, r=flip1995
add suggestions for print/write with newline lint changelog: Add machine-applicable suggestions for `print!`/`write!` with newline lints.
2 parents 46d7a0d + 2d0c797 commit 5be4e71

File tree

5 files changed

+206
-82
lines changed

5 files changed

+206
-82
lines changed

clippy_lints/src/write.rs

Lines changed: 110 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
use crate::utils::{snippet_with_applicability, span_lint, span_lint_and_sugg};
1+
use crate::utils::{snippet_with_applicability, span_lint, span_lint_and_sugg, span_lint_and_then};
22
use rustc::lint::{EarlyContext, EarlyLintPass, LintArray, LintPass};
33
use rustc::{declare_lint_pass, declare_tool_lint};
44
use rustc_errors::Applicability;
55
use std::borrow::Cow;
66
use syntax::ast::*;
77
use syntax::parse::{parser, token};
8-
use syntax::tokenstream::{TokenStream, TokenTree};
9-
use syntax_pos::symbol::Symbol;
8+
use syntax::tokenstream::TokenStream;
9+
use syntax_pos::{symbol::Symbol, BytePos, Span};
1010

1111
declare_clippy_lint! {
1212
/// **What it does:** This lint warns when you use `println!("")` to
@@ -184,8 +184,8 @@ impl EarlyLintPass for Write {
184184
fn check_mac(&mut self, cx: &EarlyContext<'_>, mac: &Mac) {
185185
if mac.node.path == sym!(println) {
186186
span_lint(cx, PRINT_STDOUT, mac.span, "use of `println!`");
187-
if let Some(fmtstr) = check_tts(cx, &mac.node.tts, false).0 {
188-
if fmtstr == "" {
187+
if let (Some(fmt_str), _) = check_tts(cx, &mac.node.tts, false) {
188+
if fmt_str.contents.is_empty() {
189189
span_lint_and_sugg(
190190
cx,
191191
PRINTLN_EMPTY_STRING,
@@ -199,35 +199,52 @@ impl EarlyLintPass for Write {
199199
}
200200
} else if mac.node.path == sym!(print) {
201201
span_lint(cx, PRINT_STDOUT, mac.span, "use of `print!`");
202-
if let (Some(fmtstr), _, is_raw) = check_tts(cx, &mac.node.tts, false) {
203-
if check_newlines(&fmtstr, is_raw) {
204-
span_lint(
202+
if let (Some(fmt_str), _) = check_tts(cx, &mac.node.tts, false) {
203+
if check_newlines(&fmt_str) {
204+
span_lint_and_then(
205205
cx,
206206
PRINT_WITH_NEWLINE,
207207
mac.span,
208-
"using `print!()` with a format string that ends in a \
209-
single newline, consider using `println!()` instead",
208+
"using `print!()` with a format string that ends in a single newline",
209+
|err| {
210+
err.multipart_suggestion(
211+
"use `println!` instead",
212+
vec![
213+
(mac.node.path.span, String::from("println")),
214+
(fmt_str.newline_span(), String::new()),
215+
],
216+
Applicability::MachineApplicable,
217+
);
218+
},
210219
);
211220
}
212221
}
213222
} else if mac.node.path == sym!(write) {
214-
if let (Some(fmtstr), _, is_raw) = check_tts(cx, &mac.node.tts, true) {
215-
if check_newlines(&fmtstr, is_raw) {
216-
span_lint(
223+
if let (Some(fmt_str), _) = check_tts(cx, &mac.node.tts, true) {
224+
if check_newlines(&fmt_str) {
225+
span_lint_and_then(
217226
cx,
218227
WRITE_WITH_NEWLINE,
219228
mac.span,
220-
"using `write!()` with a format string that ends in a \
221-
single newline, consider using `writeln!()` instead",
222-
);
229+
"using `write!()` with a format string that ends in a single newline",
230+
|err| {
231+
err.multipart_suggestion(
232+
"use `writeln!()` instead",
233+
vec![
234+
(mac.node.path.span, String::from("writeln")),
235+
(fmt_str.newline_span(), String::new()),
236+
],
237+
Applicability::MachineApplicable,
238+
);
239+
},
240+
)
223241
}
224242
}
225243
} else if mac.node.path == sym!(writeln) {
226-
let check_tts = check_tts(cx, &mac.node.tts, true);
227-
if let Some(fmtstr) = check_tts.0 {
228-
if fmtstr == "" {
244+
if let (Some(fmt_str), expr) = check_tts(cx, &mac.node.tts, true) {
245+
if fmt_str.contents.is_empty() {
229246
let mut applicability = Applicability::MachineApplicable;
230-
let suggestion = check_tts.1.map_or_else(
247+
let suggestion = expr.map_or_else(
231248
move || {
232249
applicability = Applicability::HasPlaceholders;
233250
Cow::Borrowed("v")
@@ -250,10 +267,44 @@ impl EarlyLintPass for Write {
250267
}
251268
}
252269

270+
/// The arguments of a `print[ln]!` or `write[ln]!` invocation.
271+
struct FmtStr {
272+
/// The contents of the format string (inside the quotes).
273+
contents: String,
274+
style: StrStyle,
275+
/// The span of the format string, including quotes, the raw marker, and any raw hashes.
276+
span: Span,
277+
}
278+
279+
impl FmtStr {
280+
/// Given a format string that ends in a newline and its span, calculates the span of the
281+
/// newline.
282+
fn newline_span(&self) -> Span {
283+
let sp = self.span;
284+
285+
let newline_sp_hi = sp.hi()
286+
- match self.style {
287+
StrStyle::Cooked => BytePos(1),
288+
StrStyle::Raw(hashes) => BytePos((1 + hashes).into()),
289+
};
290+
291+
let newline_sp_len = if self.contents.ends_with('\n') {
292+
BytePos(1)
293+
} else if self.contents.ends_with(r"\n") {
294+
BytePos(2)
295+
} else {
296+
panic!("expected format string to contain a newline");
297+
};
298+
299+
sp.with_lo(newline_sp_hi - newline_sp_len).with_hi(newline_sp_hi)
300+
}
301+
}
302+
253303
/// Checks the arguments of `print[ln]!` and `write[ln]!` calls. It will return a tuple of two
254-
/// options and a bool. The first part of the tuple is `format_str` of the macros. The second part
255-
/// of the tuple is in the `write[ln]!` case the expression the `format_str` should be written to.
256-
/// The final part is a boolean flag indicating if the string is a raw string.
304+
/// `Option`s. The first `Option` of the tuple is the macro's format string. It includes
305+
/// the contents of the string, whether it's a raw string, and the span of the literal in the
306+
/// source. The second `Option` in the tuple is, in the `write[ln]!` case, the expression the
307+
/// `format_str` should be written to.
257308
///
258309
/// Example:
259310
///
@@ -266,49 +317,36 @@ impl EarlyLintPass for Write {
266317
/// ```
267318
/// will return
268319
/// ```rust,ignore
269-
/// (Some("string to write: {}"), Some(buf), false)
320+
/// (Some("string to write: {}"), Some(buf))
270321
/// ```
271-
fn check_tts<'a>(cx: &EarlyContext<'a>, tts: &TokenStream, is_write: bool) -> (Option<String>, Option<Expr>, bool) {
322+
fn check_tts<'a>(cx: &EarlyContext<'a>, tts: &TokenStream, is_write: bool) -> (Option<FmtStr>, Option<Expr>) {
272323
use fmt_macros::*;
273324
let tts = tts.clone();
274-
let mut is_raw = false;
275-
if let TokenStream(Some(tokens)) = &tts {
276-
for token in tokens.iter() {
277-
if let (TokenTree::Token(_, token::Token::Literal(lit)), _) = token {
278-
match lit.kind {
279-
token::Str => break,
280-
token::StrRaw(_) => {
281-
is_raw = true;
282-
break;
283-
},
284-
_ => {},
285-
}
286-
}
287-
}
288-
}
325+
289326
let mut parser = parser::Parser::new(&cx.sess.parse_sess, tts, None, false, false, None);
290327
let mut expr: Option<Expr> = None;
291328
if is_write {
292329
expr = match parser.parse_expr().map_err(|mut err| err.cancel()) {
293330
Ok(p) => Some(p.into_inner()),
294-
Err(_) => return (None, None, is_raw),
331+
Err(_) => return (None, None),
295332
};
296333
// might be `writeln!(foo)`
297334
if parser.expect(&token::Comma).map_err(|mut err| err.cancel()).is_err() {
298-
return (None, expr, is_raw);
335+
return (None, expr);
299336
}
300337
}
301338

302-
let fmtstr = match parser.parse_str().map_err(|mut err| err.cancel()) {
303-
Ok(token) => token.0.to_string(),
304-
Err(_) => return (None, expr, is_raw),
339+
let (fmtstr, fmtstyle) = match parser.parse_str().map_err(|mut err| err.cancel()) {
340+
Ok((fmtstr, fmtstyle)) => (fmtstr.to_string(), fmtstyle),
341+
Err(_) => return (None, expr),
305342
};
343+
let fmtspan = parser.prev_span;
306344
let tmp = fmtstr.clone();
307345
let mut args = vec![];
308346
let mut fmt_parser = Parser::new(&tmp, None, Vec::new(), false);
309347
while let Some(piece) = fmt_parser.next() {
310348
if !fmt_parser.errors.is_empty() {
311-
return (None, expr, is_raw);
349+
return (None, expr);
312350
}
313351
if let Piece::NextArgument(arg) = piece {
314352
if arg.format.ty == "?" {
@@ -330,11 +368,26 @@ fn check_tts<'a>(cx: &EarlyContext<'a>, tts: &TokenStream, is_write: bool) -> (O
330368
ty: "",
331369
};
332370
if !parser.eat(&token::Comma) {
333-
return (Some(fmtstr), expr, is_raw);
371+
return (
372+
Some(FmtStr {
373+
contents: fmtstr,
374+
style: fmtstyle,
375+
span: fmtspan,
376+
}),
377+
expr,
378+
);
334379
}
335-
let token_expr = match parser.parse_expr().map_err(|mut err| err.cancel()) {
336-
Ok(expr) => expr,
337-
Err(_) => return (Some(fmtstr), None, is_raw),
380+
let token_expr = if let Ok(expr) = parser.parse_expr().map_err(|mut err| err.cancel()) {
381+
expr
382+
} else {
383+
return (
384+
Some(FmtStr {
385+
contents: fmtstr,
386+
style: fmtstyle,
387+
span: fmtspan,
388+
}),
389+
None,
390+
);
338391
};
339392
match &token_expr.node {
340393
ExprKind::Lit(_) => {
@@ -383,12 +436,15 @@ fn check_tts<'a>(cx: &EarlyContext<'a>, tts: &TokenStream, is_write: bool) -> (O
383436
}
384437
}
385438

386-
// Checks if `s` constains a single newline that terminates it
387-
// Literal and escaped newlines are both checked (only literal for raw strings)
388-
fn check_newlines(s: &str, is_raw: bool) -> bool {
439+
/// Checks if the format string constains a single newline that terminates it.
440+
///
441+
/// Literal and escaped newlines are both checked (only literal for raw strings).
442+
fn check_newlines(fmt_str: &FmtStr) -> bool {
443+
let s = &fmt_str.contents;
444+
389445
if s.ends_with('\n') {
390446
return true;
391-
} else if is_raw {
447+
} else if let StrStyle::Raw(_) = fmt_str.style {
392448
return false;
393449
}
394450

tests/ui/print_with_newline.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// FIXME: Ideally these suggestions would be fixed via rustfix. Blocked by rust-lang/rust#53934
2+
// // run-rustfix
3+
14
#![allow(clippy::print_literal)]
25
#![warn(clippy::print_with_newline)]
36

tests/ui/print_with_newline.stderr

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,82 @@
1-
error: using `print!()` with a format string that ends in a single newline, consider using `println!()` instead
2-
--> $DIR/print_with_newline.rs:5:5
1+
error: using `print!()` with a format string that ends in a single newline
2+
--> $DIR/print_with_newline.rs:8:5
33
|
44
LL | print!("Hello/n");
55
| ^^^^^^^^^^^^^^^^^
66
|
77
= note: `-D clippy::print-with-newline` implied by `-D warnings`
8+
help: use `println!` instead
9+
|
10+
LL | println!("Hello");
11+
| ^^^^^^^ --
812

9-
error: using `print!()` with a format string that ends in a single newline, consider using `println!()` instead
10-
--> $DIR/print_with_newline.rs:6:5
13+
error: using `print!()` with a format string that ends in a single newline
14+
--> $DIR/print_with_newline.rs:9:5
1115
|
1216
LL | print!("Hello {}/n", "world");
1317
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18+
help: use `println!` instead
19+
|
20+
LL | println!("Hello {}", "world");
21+
| ^^^^^^^ --
1422

15-
error: using `print!()` with a format string that ends in a single newline, consider using `println!()` instead
16-
--> $DIR/print_with_newline.rs:7:5
23+
error: using `print!()` with a format string that ends in a single newline
24+
--> $DIR/print_with_newline.rs:10:5
1725
|
1826
LL | print!("Hello {} {}/n", "world", "#2");
1927
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28+
help: use `println!` instead
29+
|
30+
LL | println!("Hello {} {}", "world", "#2");
31+
| ^^^^^^^ --
2032

21-
error: using `print!()` with a format string that ends in a single newline, consider using `println!()` instead
22-
--> $DIR/print_with_newline.rs:8:5
33+
error: using `print!()` with a format string that ends in a single newline
34+
--> $DIR/print_with_newline.rs:11:5
2335
|
2436
LL | print!("{}/n", 1265);
2537
| ^^^^^^^^^^^^^^^^^^^^
38+
help: use `println!` instead
39+
|
40+
LL | println!("{}", 1265);
41+
| ^^^^^^^ --
2642

27-
error: using `print!()` with a format string that ends in a single newline, consider using `println!()` instead
28-
--> $DIR/print_with_newline.rs:27:5
43+
error: using `print!()` with a format string that ends in a single newline
44+
--> $DIR/print_with_newline.rs:30:5
2945
|
3046
LL | print!("//n"); // should fail
3147
| ^^^^^^^^^^^^^^
48+
help: use `println!` instead
49+
|
50+
LL | println!("/"); // should fail
51+
| ^^^^^^^ --
3252

33-
error: using `print!()` with a format string that ends in a single newline, consider using `println!()` instead
34-
--> $DIR/print_with_newline.rs:34:5
53+
error: using `print!()` with a format string that ends in a single newline
54+
--> $DIR/print_with_newline.rs:37:5
3555
|
3656
LL | / print!(
3757
LL | | "
3858
LL | | "
3959
LL | | );
4060
| |_____^
61+
help: use `println!` instead
62+
|
63+
LL | println!(
64+
LL | ""
65+
|
4166

42-
error: using `print!()` with a format string that ends in a single newline, consider using `println!()` instead
43-
--> $DIR/print_with_newline.rs:38:5
67+
error: using `print!()` with a format string that ends in a single newline
68+
--> $DIR/print_with_newline.rs:41:5
4469
|
4570
LL | / print!(
4671
LL | | r"
4772
LL | | "
4873
LL | | );
4974
| |_____^
75+
help: use `println!` instead
76+
|
77+
LL | println!(
78+
LL | r""
79+
|
5080

5181
error: aborting due to 7 previous errors
5282

tests/ui/write_with_newline.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// FIXME: Ideally these suggestions would be fixed via rustfix. Blocked by rust-lang/rust#53934
2+
// // run-rustfix
3+
14
#![allow(clippy::write_literal)]
25
#![warn(clippy::write_with_newline)]
36

0 commit comments

Comments
 (0)