rust/clippy_lints/src/octal_escapes.rs

118 lines
5.1 KiB
Rust

use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::source::SpanRangeExt;
use rustc_ast::token::LitKind;
use rustc_ast::{Expr, ExprKind};
use rustc_errors::Applicability;
use rustc_lint::{EarlyContext, EarlyLintPass, LintContext};
use rustc_middle::lint::in_external_macro;
use rustc_session::declare_lint_pass;
use rustc_span::{BytePos, Pos, SpanData};
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
/// ```no_run
/// let one = "\033[1m Bold? \033[0m"; // \033 intended as escape
/// let two = "\033\0"; // \033 intended as null-3-3
/// ```
///
/// Use instead:
/// ```no_run
/// 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 let ExprKind::Lit(lit) = &expr.kind
// The number of bytes from the start of the token to the start of literal's text.
&& let start_offset = BytePos::from_u32(match lit.kind {
LitKind::Str => 1,
LitKind::ByteStr | LitKind::CStr => 2,
_ => return,
})
&& !in_external_macro(cx.sess(), expr.span)
{
let s = lit.symbol.as_str();
let mut iter = s.as_bytes().iter();
while let Some(&c) = iter.next() {
if c == b'\\'
// Always move the iterator to read the escape char.
&& let Some(b'0') = iter.next()
{
// C-style octal escapes read from one to three characters.
// The first character (`0`) has already been read.
let (tail, len, c_hi, c_lo) = match *iter.as_slice() {
[c_hi @ b'0'..=b'7', c_lo @ b'0'..=b'7', ref tail @ ..] => (tail, 4, c_hi, c_lo),
[c_lo @ b'0'..=b'7', ref tail @ ..] => (tail, 3, b'0', c_lo),
_ => continue,
};
iter = tail.iter();
let offset = start_offset + BytePos::from_usize(s.len() - tail.len());
let data = expr.span.data();
let span = SpanData {
lo: data.lo + offset - BytePos::from_u32(len),
hi: data.lo + offset,
..data
}
.span();
// Last check to make sure the source text matches what we read from the string.
// Macros are involved somehow if this doesn't match.
if span.check_source_text(cx, |src| match *src.as_bytes() {
[b'\\', b'0', lo] => lo == c_lo,
[b'\\', b'0', hi, lo] => hi == c_hi && lo == c_lo,
_ => false,
}) {
span_lint_and_then(cx, OCTAL_ESCAPES, span, "octal-looking escape in a literal", |diag| {
diag.help_once("octal escapes are not supported, `\\0` is always null")
.span_suggestion(
span,
"if an octal escape is intended, use a hex escape instead",
format!("\\x{:02x}", (((c_hi - b'0') << 3) | (c_lo - b'0'))),
Applicability::MaybeIncorrect,
)
.span_suggestion(
span,
"if a null escape is intended, disambiguate using",
format!("\\x00{}{}", c_hi as char, c_lo as char),
Applicability::MaybeIncorrect,
);
});
} else {
break;
}
}
}
}
}
}