diff --git a/clippy_lints/src/octal_escapes.rs b/clippy_lints/src/octal_escapes.rs index b3a4ab121e1..0e27be25985 100644 --- a/clippy_lints/src/octal_escapes.rs +++ b/clippy_lints/src/octal_escapes.rs @@ -6,6 +6,7 @@ use rustc_lint::{EarlyContext, EarlyLintPass}; use rustc_middle::lint::in_external_macro; use rustc_session::{declare_lint_pass, declare_tool_lint}; use rustc_span::Span; +use std::fmt::Write; declare_clippy_lint! { /// ### What it does @@ -13,9 +14,14 @@ declare_clippy_lint! { /// character escapes in C. /// /// ### Why is this bad? - /// Rust does not support octal notation for character escapes. `\0` is always a - /// null byte/character, and any following digits do not form part of the escape - /// sequence. + /// + /// C and other languages support octal character escapes in strings, where + /// a backslash is followed by up to three octal digits. For example, `\033` + /// stands for the ASCII character 27 (ESC). Rust does not support this + /// notation, but has the escape code `\0` which stands for a null + /// byte/character, and any following digits do not form part of the escape + /// sequence. Therefore, `\033` is not a compiler error but the result may + /// be surprising. /// /// ### Known problems /// The actual meaning can be the intended one. `\x00` can be used in these @@ -58,8 +64,9 @@ impl EarlyLintPass for OctalEscapes { fn check_lit(cx: &EarlyContext<'tcx>, lit: &Lit, span: Span, is_string: bool) { let contents = lit.symbol.as_str(); let mut iter = contents.char_indices().peekable(); + let mut found = vec![]; - // go through the string, looking for \0[0-7] + // go through the string, looking for \0[0-7][0-7]? while let Some((from, ch)) = iter.next() { if ch == '\\' { if let Some((_, '0')) = iter.next() { @@ -68,19 +75,41 @@ fn check_lit(cx: &EarlyContext<'tcx>, lit: &Lit, span: Span, is_string: bool) { if let Some((_, '0'..='7')) = iter.peek() { to += 1; } - emit(cx, &contents, from, to + 1, span, is_string); + found.push((from, to + 1)); } } } } -} -fn emit(cx: &EarlyContext<'tcx>, contents: &str, from: usize, to: usize, span: Span, is_string: bool) { - // construct a replacement escape for that case that octal was intended - let escape = &contents[from + 1..to]; - // the maximum value is \077, or \x3f - let literal_suggestion = u8::from_str_radix(escape, 8).ok().map(|n| format!("\\x{:02x}", n)); - let prefix = if is_string { "" } else { "b" }; + if found.is_empty() { + return; + } + + // construct two suggestion strings, one with \x escapes with octal meaning + // as in C, and one with \x00 for null bytes. + let mut suggest_1 = if is_string { "\"" } else { "b\"" }.to_string(); + let mut suggest_2 = suggest_1.clone(); + let mut index = 0; + for (from, to) in found { + suggest_1.push_str(&contents[index..from]); + suggest_2.push_str(&contents[index..from]); + + // construct a replacement escape + // the maximum value is \077, or \x3f, so u8 is sufficient here + if let Ok(n) = u8::from_str_radix(&contents[from + 1..to], 8) { + write!(&mut suggest_1, "\\x{:02x}", n).unwrap(); + } + + // append the null byte as \x00 and the following digits literally + suggest_2.push_str("\\x00"); + suggest_2.push_str(&contents[from + 2..to]); + + index = to; + } + suggest_1.push_str(&contents[index..]); + suggest_1.push('"'); + suggest_2.push_str(&contents[index..]); + suggest_2.push('"'); span_lint_and_then( cx, @@ -96,14 +125,12 @@ fn emit(cx: &EarlyContext<'tcx>, contents: &str, from: usize, to: usize, span: S if is_string { "character" } else { "byte" } )); // suggestion 1: equivalent hex escape - if let Some(sugg) = literal_suggestion { - diag.span_suggestion( - span, - "if an octal escape was intended, use the hexadecimal representation instead", - format!("{}\"{}{}{}\"", prefix, &contents[..from], sugg, &contents[to..]), - Applicability::MaybeIncorrect, - ); - } + diag.span_suggestion( + span, + "if an octal escape was intended, use the hexadecimal representation instead", + suggest_1, + Applicability::MaybeIncorrect, + ); // suggestion 2: unambiguous null byte diag.span_suggestion( span, @@ -111,7 +138,7 @@ fn emit(cx: &EarlyContext<'tcx>, contents: &str, from: usize, to: usize, span: S "if the null {} is intended, disambiguate using", if is_string { "character" } else { "byte" } ), - format!("{}\"{}\\x00{}\"", prefix, &contents[..from], &contents[from + 2..]), + suggest_2, Applicability::MaybeIncorrect, ); }, diff --git a/tests/ui/octal_escapes.stderr b/tests/ui/octal_escapes.stderr index 8c27dc0cf05..54f5bbb0fc4 100644 --- a/tests/ui/octal_escapes.stderr +++ b/tests/ui/octal_escapes.stderr @@ -72,28 +72,12 @@ LL | let _bad6 = "Text-/055/077-MoreText"; = help: octal escapes are not supported, `/0` is always a null character help: if an octal escape was intended, use the hexadecimal representation instead | -LL | let _bad6 = "Text-/x2d/077-MoreText"; +LL | let _bad6 = "Text-/x2d/x3f-MoreText"; | ~~~~~~~~~~~~~~~~~~~~~~~~ help: if the null character is intended, disambiguate using | -LL | let _bad6 = "Text-/x0055/077-MoreText"; - | ~~~~~~~~~~~~~~~~~~~~~~~~~~ - -error: octal-looking escape in string literal - --> $DIR/octal_escapes.rs:10:17 - | -LL | let _bad6 = "Text-/055/077-MoreText"; - | ^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: octal escapes are not supported, `/0` is always a null character -help: if an octal escape was intended, use the hexadecimal representation instead - | -LL | let _bad6 = "Text-/055/x3f-MoreText"; - | ~~~~~~~~~~~~~~~~~~~~~~~~ -help: if the null character is intended, disambiguate using - | -LL | let _bad6 = "Text-/055/x0077-MoreText"; - | ~~~~~~~~~~~~~~~~~~~~~~~~~~ +LL | let _bad6 = "Text-/x0055/x0077-MoreText"; + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ error: octal-looking escape in string literal --> $DIR/octal_escapes.rs:11:17 @@ -104,28 +88,12 @@ LL | let _bad7 = "EvenMoreText-/01/02-ShortEscapes"; = help: octal escapes are not supported, `/0` is always a null character help: if an octal escape was intended, use the hexadecimal representation instead | -LL | let _bad7 = "EvenMoreText-/x01/02-ShortEscapes"; - | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +LL | let _bad7 = "EvenMoreText-/x01/x02-ShortEscapes"; + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ help: if the null character is intended, disambiguate using | -LL | let _bad7 = "EvenMoreText-/x001/02-ShortEscapes"; - | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -error: octal-looking escape in string literal - --> $DIR/octal_escapes.rs:11:17 - | -LL | let _bad7 = "EvenMoreText-/01/02-ShortEscapes"; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = help: octal escapes are not supported, `/0` is always a null character -help: if an octal escape was intended, use the hexadecimal representation instead - | -LL | let _bad7 = "EvenMoreText-/01/x02-ShortEscapes"; - | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -help: if the null character is intended, disambiguate using - | -LL | let _bad7 = "EvenMoreText-/01/x002-ShortEscapes"; - | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +LL | let _bad7 = "EvenMoreText-/x001/x002-ShortEscapes"; + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ error: octal-looking escape in string literal --> $DIR/octal_escapes.rs:12:17 @@ -159,5 +127,5 @@ help: if the null character is intended, disambiguate using LL | let _bad9 = "锈/x0011锈"; | ~~~~~~~~~~~~ -error: aborting due to 10 previous errors +error: aborting due to 8 previous errors