| 
1 | 1 | use clippy_utils::diagnostics::span_lint_and_then;  | 
2 |  | -use rustc_ast::ast::{Expr, ExprKind};  | 
3 |  | -use rustc_ast::token::{Lit, LitKind};  | 
 | 2 | +use clippy_utils::source::get_source_text;  | 
 | 3 | +use rustc_ast::token::LitKind;  | 
 | 4 | +use rustc_ast::{Expr, ExprKind};  | 
4 | 5 | use rustc_errors::Applicability;  | 
5 | 6 | use rustc_lint::{EarlyContext, EarlyLintPass, LintContext};  | 
6 | 7 | use rustc_middle::lint::in_external_macro;  | 
7 | 8 | use rustc_session::declare_lint_pass;  | 
8 |  | -use rustc_span::Span;  | 
9 |  | -use std::fmt::Write;  | 
 | 9 | +use rustc_span::{BytePos, Pos, SpanData};  | 
10 | 10 | 
 
  | 
11 | 11 | declare_clippy_lint! {  | 
12 | 12 |     /// ### What it does  | 
@@ -52,104 +52,69 @@ declare_lint_pass!(OctalEscapes => [OCTAL_ESCAPES]);  | 
52 | 52 | 
 
  | 
53 | 53 | impl EarlyLintPass for OctalEscapes {  | 
54 | 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(token_lit) = &expr.kind {  | 
60 |  | -            if matches!(token_lit.kind, LitKind::Str) {  | 
61 |  | -                check_lit(cx, token_lit, expr.span, true);  | 
62 |  | -            } else if matches!(token_lit.kind, LitKind::ByteStr) {  | 
63 |  | -                check_lit(cx, token_lit, expr.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![];  | 
 | 55 | +        if let ExprKind::Lit(lit) = &expr.kind  | 
 | 56 | +            // The number of bytes from the start of the token to the start of literal's text.  | 
 | 57 | +            && let start_offset = BytePos::from_u32(match lit.kind {  | 
 | 58 | +                LitKind::Str => 1,  | 
 | 59 | +                LitKind::ByteStr | LitKind::CStr => 2,  | 
 | 60 | +                _ => return,  | 
 | 61 | +            })  | 
 | 62 | +            && !in_external_macro(cx.sess(), expr.span)  | 
 | 63 | +        {  | 
 | 64 | +            let s = lit.symbol.as_str();  | 
 | 65 | +            let mut iter = s.as_bytes().iter();  | 
 | 66 | +            while let Some(&c) = iter.next() {  | 
 | 67 | +                if c == b'\\'  | 
 | 68 | +                    // Always move the iterator to read the escape char.  | 
 | 69 | +                    && let Some(b'0') = iter.next()  | 
 | 70 | +                {  | 
 | 71 | +                    // C-style octal escapes read from one to three characters.  | 
 | 72 | +                    // The first character (`0`) has already been read.  | 
 | 73 | +                    let (tail, len, c_hi, c_lo) = match *iter.as_slice() {  | 
 | 74 | +                        [c_hi @ b'0'..=b'7', c_lo @ b'0'..=b'7', ref tail @ ..] => (tail, 4, c_hi, c_lo),  | 
 | 75 | +                        [c_lo @ b'0'..=b'7', ref tail @ ..] => (tail, 3, b'0', c_lo),  | 
 | 76 | +                        _ => continue,  | 
 | 77 | +                    };  | 
 | 78 | +                    iter = tail.iter();  | 
 | 79 | +                    let offset = start_offset + BytePos::from_usize(s.len() - tail.len());  | 
 | 80 | +                    let data = expr.span.data();  | 
 | 81 | +                    let span = SpanData {  | 
 | 82 | +                        lo: data.lo + offset - BytePos::from_u32(len),  | 
 | 83 | +                        hi: data.lo + offset,  | 
 | 84 | +                        ..data  | 
 | 85 | +                    }  | 
 | 86 | +                    .span();  | 
73 | 87 | 
 
  | 
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, _)) = iter.next_if(|(_, ch)| matches!(ch, '0'..='7')) {  | 
80 |  | -                    if iter.next_if(|(_, ch)| matches!(ch, '0'..='7')).is_some() {  | 
81 |  | -                        to += 1;  | 
 | 88 | +                    // Last check to make sure the source text matches what we read from the string.  | 
 | 89 | +                    // Macros are involved somehow if this doesn't match.  | 
 | 90 | +                    if let Some(src) = get_source_text(cx, span)  | 
 | 91 | +                        && let Some(src) = src.as_str()  | 
 | 92 | +                        && match *src.as_bytes() {  | 
 | 93 | +                            [b'\\', b'0', lo] => lo == c_lo,  | 
 | 94 | +                            [b'\\', b'0', hi, lo] => hi == c_hi && lo == c_lo,  | 
 | 95 | +                            _ => false,  | 
 | 96 | +                        }  | 
 | 97 | +                    {  | 
 | 98 | +                        span_lint_and_then(cx, OCTAL_ESCAPES, span, "octal-looking escape in a literal", |diag| {  | 
 | 99 | +                            diag.help_once("octal escapes are not supported, `\\0` is always null")  | 
 | 100 | +                                .span_suggestion(  | 
 | 101 | +                                    span,  | 
 | 102 | +                                    "if an octal escape is intended, use a hex escape instead",  | 
 | 103 | +                                    format!("\\x{:02x}", (((c_hi - b'0') << 3) | (c_lo - b'0'))),  | 
 | 104 | +                                    Applicability::MaybeIncorrect,  | 
 | 105 | +                                )  | 
 | 106 | +                                .span_suggestion(  | 
 | 107 | +                                    span,  | 
 | 108 | +                                    "if a null escape is intended, disambiguate using",  | 
 | 109 | +                                    format!("\\x00{}{}", c_hi as char, c_lo as char),  | 
 | 110 | +                                    Applicability::MaybeIncorrect,  | 
 | 111 | +                                );  | 
 | 112 | +                        });  | 
 | 113 | +                    } else {  | 
 | 114 | +                        break;  | 
82 | 115 |                     }  | 
83 |  | -                    found.push((from, to + 1));  | 
84 | 116 |                 }  | 
85 | 117 |             }  | 
86 | 118 |         }  | 
87 | 119 |     }  | 
88 |  | - | 
89 |  | -    if found.is_empty() {  | 
90 |  | -        return;  | 
91 |  | -    }  | 
92 |  | - | 
93 |  | -    span_lint_and_then(  | 
94 |  | -        cx,  | 
95 |  | -        OCTAL_ESCAPES,  | 
96 |  | -        span,  | 
97 |  | -        format!(  | 
98 |  | -            "octal-looking escape in {} literal",  | 
99 |  | -            if is_string { "string" } else { "byte string" }  | 
100 |  | -        ),  | 
101 |  | -        |diag| {  | 
102 |  | -            diag.help(format!(  | 
103 |  | -                "octal escapes are not supported, `\\0` is always a null {}",  | 
104 |  | -                if is_string { "character" } else { "byte" }  | 
105 |  | -            ));  | 
106 |  | - | 
107 |  | -            // Generate suggestions if the string is not too long (~ 5 lines)  | 
108 |  | -            if contents.len() < 400 {  | 
109 |  | -                // construct two suggestion strings, one with \x escapes with octal meaning  | 
110 |  | -                // as in C, and one with \x00 for null bytes.  | 
111 |  | -                let mut suggest_1 = if is_string { "\"" } else { "b\"" }.to_string();  | 
112 |  | -                let mut suggest_2 = suggest_1.clone();  | 
113 |  | -                let mut index = 0;  | 
114 |  | -                for (from, to) in found {  | 
115 |  | -                    suggest_1.push_str(&contents[index..from]);  | 
116 |  | -                    suggest_2.push_str(&contents[index..from]);  | 
117 |  | - | 
118 |  | -                    // construct a replacement escape  | 
119 |  | -                    // the maximum value is \077, or \x3f, so u8 is sufficient here  | 
120 |  | -                    if let Ok(n) = u8::from_str_radix(&contents[from + 1..to], 8) {  | 
121 |  | -                        write!(suggest_1, "\\x{n:02x}").unwrap();  | 
122 |  | -                    }  | 
123 |  | - | 
124 |  | -                    // append the null byte as \x00 and the following digits literally  | 
125 |  | -                    suggest_2.push_str("\\x00");  | 
126 |  | -                    suggest_2.push_str(&contents[from + 2..to]);  | 
127 |  | - | 
128 |  | -                    index = to;  | 
129 |  | -                }  | 
130 |  | -                suggest_1.push_str(&contents[index..]);  | 
131 |  | -                suggest_2.push_str(&contents[index..]);  | 
132 |  | - | 
133 |  | -                suggest_1.push('"');  | 
134 |  | -                suggest_2.push('"');  | 
135 |  | -                // suggestion 1: equivalent hex escape  | 
136 |  | -                diag.span_suggestion(  | 
137 |  | -                    span,  | 
138 |  | -                    "if an octal escape was intended, use the hexadecimal representation instead",  | 
139 |  | -                    suggest_1,  | 
140 |  | -                    Applicability::MaybeIncorrect,  | 
141 |  | -                );  | 
142 |  | -                // suggestion 2: unambiguous null byte  | 
143 |  | -                diag.span_suggestion(  | 
144 |  | -                    span,  | 
145 |  | -                    format!(  | 
146 |  | -                        "if the null {} is intended, disambiguate using",  | 
147 |  | -                        if is_string { "character" } else { "byte" }  | 
148 |  | -                    ),  | 
149 |  | -                    suggest_2,  | 
150 |  | -                    Applicability::MaybeIncorrect,  | 
151 |  | -                );  | 
152 |  | -            }  | 
153 |  | -        },  | 
154 |  | -    );  | 
155 | 120 | }  | 
0 commit comments