about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2021-11-22 20:29:53 +0000
committerbors <bors@rust-lang.org>2021-11-22 20:29:53 +0000
commit57a8804ef991b04aed5414fbf668f444c9050c73 (patch)
tree1b667aab85b397d1ab43160044332d8e5d241f0f
parent40275945334b71eabd87e8d651fa1cd53e274aed (diff)
parent1210bb40d35829672744c1d58f3614723a7e1bad (diff)
downloadrust-57a8804ef991b04aed5414fbf668f444c9050c73.tar.gz
rust-57a8804ef991b04aed5414fbf668f444c9050c73.zip
Auto merge of #8007 - birkenfeld:octal_escapes, r=xFrednet
Add new lint `octal_escapes`

This checks for sequences in strings that would be octal character
escapes in C, but are not supported in Rust.  It suggests either
to use the `\x00` escape, or an equivalent hex escape if the octal
was intended.

Fixes #7981

---

*Please write a short comment explaining your change (or "none" for internal only changes)*

changelog: Add new lint [`octal_escapes`], which checks for literals like `"\033[0m"`.
-rw-r--r--CHANGELOG.md1
-rw-r--r--clippy_lints/src/lib.register_all.rs1
-rw-r--r--clippy_lints/src/lib.register_lints.rs1
-rw-r--r--clippy_lints/src/lib.register_suspicious.rs1
-rw-r--r--clippy_lints/src/lib.rs2
-rw-r--r--clippy_lints/src/octal_escapes.rs150
-rw-r--r--tests/ui/octal_escapes.rs20
-rw-r--r--tests/ui/octal_escapes.stderr131
8 files changed, 307 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dadf01419f7..401557b3eac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3056,6 +3056,7 @@ Released 2018-09-13
 [`nonsensical_open_options`]: https://rust-lang.github.io/rust-clippy/master/index.html#nonsensical_open_options
 [`nonstandard_macro_braces`]: https://rust-lang.github.io/rust-clippy/master/index.html#nonstandard_macro_braces
 [`not_unsafe_ptr_arg_deref`]: https://rust-lang.github.io/rust-clippy/master/index.html#not_unsafe_ptr_arg_deref
+[`octal_escapes`]: https://rust-lang.github.io/rust-clippy/master/index.html#octal_escapes
 [`ok_expect`]: https://rust-lang.github.io/rust-clippy/master/index.html#ok_expect
 [`op_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#op_ref
 [`option_as_ref_deref`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_as_ref_deref
diff --git a/clippy_lints/src/lib.register_all.rs b/clippy_lints/src/lib.register_all.rs
index 5ed2729b99b..612240135ac 100644
--- a/clippy_lints/src/lib.register_all.rs
+++ b/clippy_lints/src/lib.register_all.rs
@@ -219,6 +219,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
     LintId::of(non_expressive_names::JUST_UNDERSCORES_AND_DIGITS),
     LintId::of(non_octal_unix_permissions::NON_OCTAL_UNIX_PERMISSIONS),
     LintId::of(non_send_fields_in_send_ty::NON_SEND_FIELDS_IN_SEND_TY),
+    LintId::of(octal_escapes::OCTAL_ESCAPES),
     LintId::of(open_options::NONSENSICAL_OPEN_OPTIONS),
     LintId::of(option_env_unwrap::OPTION_ENV_UNWRAP),
     LintId::of(overflow_check_conditional::OVERFLOW_CHECK_CONDITIONAL),
diff --git a/clippy_lints/src/lib.register_lints.rs b/clippy_lints/src/lib.register_lints.rs
index 000c7226685..19c35a5e5f4 100644
--- a/clippy_lints/src/lib.register_lints.rs
+++ b/clippy_lints/src/lib.register_lints.rs
@@ -380,6 +380,7 @@ store.register_lints(&[
     non_octal_unix_permissions::NON_OCTAL_UNIX_PERMISSIONS,
     non_send_fields_in_send_ty::NON_SEND_FIELDS_IN_SEND_TY,
     nonstandard_macro_braces::NONSTANDARD_MACRO_BRACES,
+    octal_escapes::OCTAL_ESCAPES,
     open_options::NONSENSICAL_OPEN_OPTIONS,
     option_env_unwrap::OPTION_ENV_UNWRAP,
     option_if_let_else::OPTION_IF_LET_ELSE,
diff --git a/clippy_lints/src/lib.register_suspicious.rs b/clippy_lints/src/lib.register_suspicious.rs
index a3f964d1580..414bfc42fdf 100644
--- a/clippy_lints/src/lib.register_suspicious.rs
+++ b/clippy_lints/src/lib.register_suspicious.rs
@@ -16,6 +16,7 @@ store.register_group(true, "clippy::suspicious", Some("clippy_suspicious"), vec!
     LintId::of(methods::SUSPICIOUS_MAP),
     LintId::of(mut_key::MUTABLE_KEY_TYPE),
     LintId::of(non_send_fields_in_send_ty::NON_SEND_FIELDS_IN_SEND_TY),
+    LintId::of(octal_escapes::OCTAL_ESCAPES),
     LintId::of(suspicious_trait_impl::SUSPICIOUS_ARITHMETIC_IMPL),
     LintId::of(suspicious_trait_impl::SUSPICIOUS_OP_ASSIGN_IMPL),
 ])
diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs
index 449fa8a6311..3dafdf8f0d5 100644
--- a/clippy_lints/src/lib.rs
+++ b/clippy_lints/src/lib.rs
@@ -312,6 +312,7 @@ mod non_expressive_names;
 mod non_octal_unix_permissions;
 mod non_send_fields_in_send_ty;
 mod nonstandard_macro_braces;
+mod octal_escapes;
 mod open_options;
 mod option_env_unwrap;
 mod option_if_let_else;
@@ -849,6 +850,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
     store.register_late_pass(|| Box::new(match_str_case_mismatch::MatchStrCaseMismatch));
     store.register_late_pass(move || Box::new(format_args::FormatArgs));
     store.register_late_pass(|| Box::new(trailing_empty_array::TrailingEmptyArray));
+    store.register_early_pass(|| Box::new(octal_escapes::OctalEscapes));
     // add lints here, do not remove this comment, it's used in `new_lint`
 }
 
diff --git a/clippy_lints/src/octal_escapes.rs b/clippy_lints/src/octal_escapes.rs
new file mode 100644
index 00000000000..9c971437645
--- /dev/null
+++ b/clippy_lints/src/octal_escapes.rs
@@ -0,0 +1,150 @@
+use clippy_utils::diagnostics::span_lint_and_then;
+use rustc_ast::ast::{Expr, ExprKind};
+use rustc_ast::token::{Lit, LitKind};
+use rustc_errors::Applicability;
+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
+    /// Checks for `\0` escapes in string and byte literals that look like octal
+    /// character escapes in C.
+    ///
+    /// ### Why is this bad?
+    ///
+    /// 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
+    /// cases to be unambigious.
+    ///
+    /// The lint does not trigger for format strings in `print!()`, `write!()`
+    /// and friends since the string is already preprocessed when Clippy lints
+    /// can see it.
+    ///
+    /// # Example
+    /// ```rust
+    /// // Bad
+    /// let one = "\033[1m Bold? \033[0m";  // \033 intended as escape
+    /// let two = "\033\0";                 // \033 intended as null-3-3
+    ///
+    /// // Good
+    /// let one = "\x1b[1mWill this be bold?\x1b[0m";
+    /// let two = "\x0033\x00";
+    /// ```
+    #[clippy::version = "1.58.0"]
+    pub OCTAL_ESCAPES,
+    suspicious,
+    "string escape sequences looking like octal characters"
+}
+
+declare_lint_pass!(OctalEscapes => [OCTAL_ESCAPES]);
+
+impl EarlyLintPass for OctalEscapes {
+    fn check_expr(&mut self, cx: &EarlyContext<'tcx>, expr: &Expr) {
+        if in_external_macro(cx.sess, expr.span) {
+            return;
+        }
+
+        if let ExprKind::Lit(lit) = &expr.kind {
+            if matches!(lit.token.kind, LitKind::Str) {
+                check_lit(cx, &lit.token, lit.span, true);
+            } else if matches!(lit.token.kind, LitKind::ByteStr) {
+                check_lit(cx, &lit.token, lit.span, false);
+            }
+        }
+    }
+}
+
+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][0-7]?
+    while let Some((from, ch)) = iter.next() {
+        if ch == '\\' {
+            if let Some((_, '0')) = iter.next() {
+                // collect up to two further octal digits
+                if let Some((mut to, '0'..='7')) = iter.next() {
+                    if let Some((_, '0'..='7')) = iter.peek() {
+                        to += 1;
+                    }
+                    found.push((from, to + 1));
+                }
+            }
+        }
+    }
+
+    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,
+        OCTAL_ESCAPES,
+        span,
+        &format!(
+            "octal-looking escape in {} literal",
+            if is_string { "string" } else { "byte string" }
+        ),
+        |diag| {
+            diag.help(&format!(
+                "octal escapes are not supported, `\\0` is always a null {}",
+                if is_string { "character" } else { "byte" }
+            ));
+            // suggestion 1: equivalent hex escape
+            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,
+                &format!(
+                    "if the null {} is intended, disambiguate using",
+                    if is_string { "character" } else { "byte" }
+                ),
+                suggest_2,
+                Applicability::MaybeIncorrect,
+            );
+        },
+    );
+}
diff --git a/tests/ui/octal_escapes.rs b/tests/ui/octal_escapes.rs
new file mode 100644
index 00000000000..53145ef0fd2
--- /dev/null
+++ b/tests/ui/octal_escapes.rs
@@ -0,0 +1,20 @@
+#![warn(clippy::octal_escapes)]
+
+fn main() {
+    let _bad1 = "\033[0m";
+    let _bad2 = b"\033[0m";
+    let _bad3 = "\\\033[0m";
+    // maximum 3 digits (\012 is the escape)
+    let _bad4 = "\01234567";
+    let _bad5 = "\0\03";
+    let _bad6 = "Text-\055\077-MoreText";
+    let _bad7 = "EvenMoreText-\01\02-ShortEscapes";
+    let _bad8 = "锈\01锈";
+    let _bad9 = "锈\011锈";
+
+    let _good1 = "\\033[0m";
+    let _good2 = "\0\\0";
+    let _good3 = "\0\0";
+    let _good4 = "X\0\0X";
+    let _good5 = "锈\0锈";
+}
diff --git a/tests/ui/octal_escapes.stderr b/tests/ui/octal_escapes.stderr
new file mode 100644
index 00000000000..54f5bbb0fc4
--- /dev/null
+++ b/tests/ui/octal_escapes.stderr
@@ -0,0 +1,131 @@
+error: octal-looking escape in string literal
+  --> $DIR/octal_escapes.rs:4:17
+   |
+LL |     let _bad1 = "/033[0m";
+   |                 ^^^^^^^^^
+   |
+   = note: `-D clippy::octal-escapes` implied by `-D warnings`
+   = 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 _bad1 = "/x1b[0m";
+   |                 ~~~~~~~~~
+help: if the null character is intended, disambiguate using
+   |
+LL |     let _bad1 = "/x0033[0m";
+   |                 ~~~~~~~~~~~
+
+error: octal-looking escape in byte string literal
+  --> $DIR/octal_escapes.rs:5:17
+   |
+LL |     let _bad2 = b"/033[0m";
+   |                 ^^^^^^^^^^
+   |
+   = help: octal escapes are not supported, `/0` is always a null byte
+help: if an octal escape was intended, use the hexadecimal representation instead
+   |
+LL |     let _bad2 = b"/x1b[0m";
+   |                 ~~~~~~~~~~
+help: if the null byte is intended, disambiguate using
+   |
+LL |     let _bad2 = b"/x0033[0m";
+   |                 ~~~~~~~~~~~~
+
+error: octal-looking escape in string literal
+  --> $DIR/octal_escapes.rs:6:17
+   |
+LL |     let _bad3 = "//033[0m";
+   |                 ^^^^^^^^^^^
+   |
+   = 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 _bad3 = "//x1b[0m";
+   |                 ~~~~~~~~~~~
+help: if the null character is intended, disambiguate using
+   |
+LL |     let _bad3 = "//x0033[0m";
+   |                 ~~~~~~~~~~~~~
+
+error: octal-looking escape in string literal
+  --> $DIR/octal_escapes.rs:8:17
+   |
+LL |     let _bad4 = "/01234567";
+   |                 ^^^^^^^^^^^
+   |
+   = 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 _bad4 = "/x0a34567";
+   |                 ~~~~~~~~~~~
+help: if the null character is intended, disambiguate using
+   |
+LL |     let _bad4 = "/x001234567";
+   |                 ~~~~~~~~~~~~~
+
+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-/x2d/x3f-MoreText";
+   |                 ~~~~~~~~~~~~~~~~~~~~~~~~
+help: if the null character is intended, disambiguate using
+   |
+LL |     let _bad6 = "Text-/x0055/x0077-MoreText";
+   |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+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-/x01/x02-ShortEscapes";
+   |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+help: if the null character is intended, disambiguate using
+   |
+LL |     let _bad7 = "EvenMoreText-/x001/x002-ShortEscapes";
+   |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+error: octal-looking escape in string literal
+  --> $DIR/octal_escapes.rs:12:17
+   |
+LL |     let _bad8 = "锈/01锈";
+   |                 ^^^^^^^^^
+   |
+   = 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 _bad8 = "锈/x01锈";
+   |                 ~~~~~~~~~~
+help: if the null character is intended, disambiguate using
+   |
+LL |     let _bad8 = "锈/x001锈";
+   |                 ~~~~~~~~~~~
+
+error: octal-looking escape in string literal
+  --> $DIR/octal_escapes.rs:13:17
+   |
+LL |     let _bad9 = "锈/011锈";
+   |                 ^^^^^^^^^^
+   |
+   = 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 _bad9 = "锈/x09锈";
+   |                 ~~~~~~~~~~
+help: if the null character is intended, disambiguate using
+   |
+LL |     let _bad9 = "锈/x0011锈";
+   |                 ~~~~~~~~~~~~
+
+error: aborting due to 8 previous errors
+