summaryrefslogtreecommitdiffstats
path: root/src/tools/clippy/clippy_lints/src/octal_escapes.rs
blob: bffbf20b4d289982288b8a9ffb2e7eb81d39bc97 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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, LintContext};
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 unambiguous.
    ///
    /// 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
    /// let one = "\033[1m Bold? \033[0m";  // \033 intended as escape
    /// let two = "\033\0";                 // \033 intended as null-3-3
    /// ```
    ///
    /// Use instead:
    /// ```rust
    /// let one = "\x1b[1mWill this be bold?\x1b[0m";
    /// let two = "\x0033\x00";
    /// ```
    #[clippy::version = "1.59.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<'_>, expr: &Expr) {
        if in_external_macro(cx.sess(), expr.span) {
            return;
        }

        if let ExprKind::Lit(lit) = &expr.kind {
            if matches!(lit.token_lit.kind, LitKind::Str) {
                check_lit(cx, &lit.token_lit, lit.span, true);
            } else if matches!(lit.token_lit.kind, LitKind::ByteStr) {
                check_lit(cx, &lit.token_lit, lit.span, false);
            }
        }
    }
}

fn check_lit(cx: &EarlyContext<'_>, 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!(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,
            );
        },
    );
}