]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/octal_escapes.rs
Merge commit 'd7b5cbf065b88830ca519adcb73fad4c0d24b1c7' into clippyup
[rust.git] / src / tools / clippy / 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, LintContext};
6 use rustc_middle::lint::in_external_macro;
7 use rustc_session::{declare_lint_pass, declare_tool_lint};
8 use rustc_span::Span;
9 use std::fmt::Write;
10
11 declare_clippy_lint! {
12     /// ### What it does
13     /// Checks for `\0` escapes in string and byte literals that look like octal
14     /// character escapes in C.
15     ///
16     /// ### Why is this bad?
17     ///
18     /// C and other languages support octal character escapes in strings, where
19     /// a backslash is followed by up to three octal digits. For example, `\033`
20     /// stands for the ASCII character 27 (ESC). Rust does not support this
21     /// notation, but has the escape code `\0` which stands for a null
22     /// byte/character, and any following digits do not form part of the escape
23     /// sequence. Therefore, `\033` is not a compiler error but the result may
24     /// be surprising.
25     ///
26     /// ### Known problems
27     /// The actual meaning can be the intended one. `\x00` can be used in these
28     /// cases to be unambiguous.
29     ///
30     /// The lint does not trigger for format strings in `print!()`, `write!()`
31     /// and friends since the string is already preprocessed when Clippy lints
32     /// can see it.
33     ///
34     /// # Example
35     /// ```rust
36     /// let one = "\033[1m Bold? \033[0m";  // \033 intended as escape
37     /// let two = "\033\0";                 // \033 intended as null-3-3
38     /// ```
39     ///
40     /// Use instead:
41     /// ```rust
42     /// let one = "\x1b[1mWill this be bold?\x1b[0m";
43     /// let two = "\x0033\x00";
44     /// ```
45     #[clippy::version = "1.59.0"]
46     pub OCTAL_ESCAPES,
47     suspicious,
48     "string escape sequences looking like octal characters"
49 }
50
51 declare_lint_pass!(OctalEscapes => [OCTAL_ESCAPES]);
52
53 impl EarlyLintPass for OctalEscapes {
54     fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) {
55         if in_external_macro(cx.sess(), expr.span) {
56             return;
57         }
58
59         if let ExprKind::Lit(lit) = &expr.kind {
60             if matches!(lit.token.kind, LitKind::Str) {
61                 check_lit(cx, &lit.token, lit.span, true);
62             } else if matches!(lit.token.kind, LitKind::ByteStr) {
63                 check_lit(cx, &lit.token, lit.span, false);
64             }
65         }
66     }
67 }
68
69 fn check_lit(cx: &EarlyContext<'_>, lit: &Lit, span: Span, is_string: bool) {
70     let contents = lit.symbol.as_str();
71     let mut iter = contents.char_indices().peekable();
72     let mut found = vec![];
73
74     // go through the string, looking for \0[0-7][0-7]?
75     while let Some((from, ch)) = iter.next() {
76         if ch == '\\' {
77             if let Some((_, '0')) = iter.next() {
78                 // collect up to two further octal digits
79                 if let Some((mut to, '0'..='7')) = iter.next() {
80                     if let Some((_, '0'..='7')) = iter.peek() {
81                         to += 1;
82                     }
83                     found.push((from, to + 1));
84                 }
85             }
86         }
87     }
88
89     if found.is_empty() {
90         return;
91     }
92
93     // construct two suggestion strings, one with \x escapes with octal meaning
94     // as in C, and one with \x00 for null bytes.
95     let mut suggest_1 = if is_string { "\"" } else { "b\"" }.to_string();
96     let mut suggest_2 = suggest_1.clone();
97     let mut index = 0;
98     for (from, to) in found {
99         suggest_1.push_str(&contents[index..from]);
100         suggest_2.push_str(&contents[index..from]);
101
102         // construct a replacement escape
103         // the maximum value is \077, or \x3f, so u8 is sufficient here
104         if let Ok(n) = u8::from_str_radix(&contents[from + 1..to], 8) {
105             write!(suggest_1, "\\x{:02x}", n).unwrap();
106         }
107
108         // append the null byte as \x00 and the following digits literally
109         suggest_2.push_str("\\x00");
110         suggest_2.push_str(&contents[from + 2..to]);
111
112         index = to;
113     }
114     suggest_1.push_str(&contents[index..]);
115     suggest_1.push('"');
116     suggest_2.push_str(&contents[index..]);
117     suggest_2.push('"');
118
119     span_lint_and_then(
120         cx,
121         OCTAL_ESCAPES,
122         span,
123         &format!(
124             "octal-looking escape in {} literal",
125             if is_string { "string" } else { "byte string" }
126         ),
127         |diag| {
128             diag.help(&format!(
129                 "octal escapes are not supported, `\\0` is always a null {}",
130                 if is_string { "character" } else { "byte" }
131             ));
132             // suggestion 1: equivalent hex escape
133             diag.span_suggestion(
134                 span,
135                 "if an octal escape was intended, use the hexadecimal representation instead",
136                 suggest_1,
137                 Applicability::MaybeIncorrect,
138             );
139             // suggestion 2: unambiguous null byte
140             diag.span_suggestion(
141                 span,
142                 &format!(
143                     "if the null {} is intended, disambiguate using",
144                     if is_string { "character" } else { "byte" }
145                 ),
146                 suggest_2,
147                 Applicability::MaybeIncorrect,
148             );
149         },
150     );
151 }