]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/octal_escapes.rs
Add new lint `octal_escapes`
[rust.git] / clippy_lints / src / octal_escapes.rs
1 use clippy_utils::diagnostics::span_lint_and_then;
2 use rustc_ast::ast::{Expr, ExprKind};
3 use rustc_ast::token::{Lit, LitKind};
4 use rustc_errors::Applicability;
5 use rustc_lint::{EarlyContext, EarlyLintPass};
6 use rustc_middle::lint::in_external_macro;
7 use rustc_session::{declare_lint_pass, declare_tool_lint};
8 use rustc_span::Span;
9
10 declare_clippy_lint! {
11     /// ### What it does
12     /// Checks for `\0` escapes in string and byte literals that look like octal character
13     /// escapes in C
14     ///
15     /// ### Why is this bad?
16     /// Rust does not support octal notation for character escapes. `\0` is always a
17     /// null byte/character, and any following digits do not form part of the escape
18     /// sequence.
19     ///
20     /// ### Known problems
21     /// The actual meaning can be the intended one. `\x00` can be used in these
22     /// cases to be unambigious.
23     ///
24     /// # Example
25     /// ```rust
26     /// // Bad
27     /// let one = "\033[1m Bold? \033[0m";  // \033 intended as escape
28     /// let two = "\033\0";                 // \033 intended as null-3-3
29     ///
30     /// // Good
31     /// let one = "\x1b[1mWill this be bold?\x1b[0m";
32     /// let two = "\x0033\x00";
33     /// ```
34     #[clippy::version = "1.58.0"]
35     pub OCTAL_ESCAPES,
36     suspicious,
37     "string escape sequences looking like octal characters"
38 }
39
40 declare_lint_pass!(OctalEscapes => [OCTAL_ESCAPES]);
41
42 impl EarlyLintPass for OctalEscapes {
43     fn check_expr(&mut self, cx: &EarlyContext<'tcx>, expr: &Expr) {
44         if in_external_macro(cx.sess, expr.span) {
45             return;
46         }
47
48         if let ExprKind::Lit(lit) = &expr.kind {
49             if matches!(lit.token.kind, LitKind::Str) {
50                 check_lit(cx, &lit.token, lit.span, true);
51             } else if matches!(lit.token.kind, LitKind::ByteStr) {
52                 check_lit(cx, &lit.token, lit.span, false);
53             }
54         }
55     }
56 }
57
58 fn check_lit(cx: &EarlyContext<'tcx>, lit: &Lit, span: Span, is_string: bool) {
59     let contents = lit.symbol.as_str();
60     let mut iter = contents.char_indices();
61
62     // go through the string, looking for \0[0-7]
63     while let Some((from, ch)) = iter.next() {
64         if ch == '\\' {
65             if let Some((mut to, '0')) = iter.next() {
66                 // collect all further potentially octal digits
67                 while let Some((j, '0'..='7')) = iter.next() {
68                     to = j + 1;
69                 }
70                 // if it's more than just `\0` we have a match
71                 if to > from + 2 {
72                     emit(cx, &contents, from, to, span, is_string);
73                     return;
74                 }
75             }
76         }
77     }
78 }
79
80 fn emit(cx: &EarlyContext<'tcx>, contents: &str, from: usize, to: usize, span: Span, is_string: bool) {
81     // construct a replacement escape for that case that octal was intended
82     let escape = &contents[from + 1..to];
83     let literal_suggestion = if is_string {
84         u32::from_str_radix(escape, 8).ok().and_then(|n| {
85             if n < 256 {
86                 Some(format!("\\x{:02x}", n))
87             } else if n <= std::char::MAX as u32 {
88                 Some(format!("\\u{{{:x}}}", n))
89             } else {
90                 None
91             }
92         })
93     } else {
94         u8::from_str_radix(escape, 8).ok().map(|n| format!("\\x{:02x}", n))
95     };
96
97     span_lint_and_then(
98         cx,
99         OCTAL_ESCAPES,
100         span,
101         &format!(
102             "octal-looking escape in {} literal",
103             if is_string { "string" } else { "byte string" }
104         ),
105         |diag| {
106             diag.help(&format!(
107                 "octal escapes are not supported, `\\0` is always a null {}",
108                 if is_string { "character" } else { "byte" }
109             ));
110             // suggestion 1: equivalent hex escape
111             if let Some(sugg) = literal_suggestion {
112                 diag.span_suggestion(
113                     span,
114                     "if an octal escape is intended, use",
115                     format!("\"{}{}{}\"", &contents[..from], sugg, &contents[to..]),
116                     Applicability::MaybeIncorrect,
117                 );
118             }
119             // suggestion 2: unambiguous null byte
120             diag.span_suggestion(
121                 span,
122                 &format!(
123                     "if the null {} is intended, disambiguate using",
124                     if is_string { "character" } else { "byte" }
125                 ),
126                 format!("\"{}\\x00{}\"", &contents[..from], &contents[from + 2..]),
127                 Applicability::MaybeIncorrect,
128             );
129         },
130     );
131 }