|
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