Skip to content

Commit 57a8804

Browse files
committed
Auto merge of #8007 - birkenfeld:octal_escapes, r=xFrednet
Add new lint `octal_escapes` This checks for sequences in strings that would be octal character escapes in C, but are not supported in Rust. It suggests either to use the `\x00` escape, or an equivalent hex escape if the octal was intended. Fixes #7981 --- *Please write a short comment explaining your change (or "none" for internal only changes)* changelog: Add new lint [`octal_escapes`], which checks for literals like `"\033[0m"`.
2 parents 4027594 + 1210bb4 commit 57a8804

8 files changed

+307
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3056,6 +3056,7 @@ Released 2018-09-13
30563056
[`nonsensical_open_options`]: https://rust-lang.github.io/rust-clippy/master/index.html#nonsensical_open_options
30573057
[`nonstandard_macro_braces`]: https://rust-lang.github.io/rust-clippy/master/index.html#nonstandard_macro_braces
30583058
[`not_unsafe_ptr_arg_deref`]: https://rust-lang.github.io/rust-clippy/master/index.html#not_unsafe_ptr_arg_deref
3059+
[`octal_escapes`]: https://rust-lang.github.io/rust-clippy/master/index.html#octal_escapes
30593060
[`ok_expect`]: https://rust-lang.github.io/rust-clippy/master/index.html#ok_expect
30603061
[`op_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#op_ref
30613062
[`option_as_ref_deref`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_as_ref_deref

clippy_lints/src/lib.register_all.rs

+1
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
219219
LintId::of(non_expressive_names::JUST_UNDERSCORES_AND_DIGITS),
220220
LintId::of(non_octal_unix_permissions::NON_OCTAL_UNIX_PERMISSIONS),
221221
LintId::of(non_send_fields_in_send_ty::NON_SEND_FIELDS_IN_SEND_TY),
222+
LintId::of(octal_escapes::OCTAL_ESCAPES),
222223
LintId::of(open_options::NONSENSICAL_OPEN_OPTIONS),
223224
LintId::of(option_env_unwrap::OPTION_ENV_UNWRAP),
224225
LintId::of(overflow_check_conditional::OVERFLOW_CHECK_CONDITIONAL),

clippy_lints/src/lib.register_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ store.register_lints(&[
380380
non_octal_unix_permissions::NON_OCTAL_UNIX_PERMISSIONS,
381381
non_send_fields_in_send_ty::NON_SEND_FIELDS_IN_SEND_TY,
382382
nonstandard_macro_braces::NONSTANDARD_MACRO_BRACES,
383+
octal_escapes::OCTAL_ESCAPES,
383384
open_options::NONSENSICAL_OPEN_OPTIONS,
384385
option_env_unwrap::OPTION_ENV_UNWRAP,
385386
option_if_let_else::OPTION_IF_LET_ELSE,

clippy_lints/src/lib.register_suspicious.rs

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ store.register_group(true, "clippy::suspicious", Some("clippy_suspicious"), vec!
1616
LintId::of(methods::SUSPICIOUS_MAP),
1717
LintId::of(mut_key::MUTABLE_KEY_TYPE),
1818
LintId::of(non_send_fields_in_send_ty::NON_SEND_FIELDS_IN_SEND_TY),
19+
LintId::of(octal_escapes::OCTAL_ESCAPES),
1920
LintId::of(suspicious_trait_impl::SUSPICIOUS_ARITHMETIC_IMPL),
2021
LintId::of(suspicious_trait_impl::SUSPICIOUS_OP_ASSIGN_IMPL),
2122
])

clippy_lints/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ mod non_expressive_names;
312312
mod non_octal_unix_permissions;
313313
mod non_send_fields_in_send_ty;
314314
mod nonstandard_macro_braces;
315+
mod octal_escapes;
315316
mod open_options;
316317
mod option_env_unwrap;
317318
mod option_if_let_else;
@@ -849,6 +850,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
849850
store.register_late_pass(|| Box::new(match_str_case_mismatch::MatchStrCaseMismatch));
850851
store.register_late_pass(move || Box::new(format_args::FormatArgs));
851852
store.register_late_pass(|| Box::new(trailing_empty_array::TrailingEmptyArray));
853+
store.register_early_pass(|| Box::new(octal_escapes::OctalEscapes));
852854
// add lints here, do not remove this comment, it's used in `new_lint`
853855
}
854856

clippy_lints/src/octal_escapes.rs

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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};
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 unambigious.
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+
/// // Bad
37+
/// let one = "\033[1m Bold? \033[0m"; // \033 intended as escape
38+
/// let two = "\033\0"; // \033 intended as null-3-3
39+
///
40+
/// // Good
41+
/// let one = "\x1b[1mWill this be bold?\x1b[0m";
42+
/// let two = "\x0033\x00";
43+
/// ```
44+
#[clippy::version = "1.58.0"]
45+
pub OCTAL_ESCAPES,
46+
suspicious,
47+
"string escape sequences looking like octal characters"
48+
}
49+
50+
declare_lint_pass!(OctalEscapes => [OCTAL_ESCAPES]);
51+
52+
impl EarlyLintPass for OctalEscapes {
53+
fn check_expr(&mut self, cx: &EarlyContext<'tcx>, expr: &Expr) {
54+
if in_external_macro(cx.sess, expr.span) {
55+
return;
56+
}
57+
58+
if let ExprKind::Lit(lit) = &expr.kind {
59+
if matches!(lit.token.kind, LitKind::Str) {
60+
check_lit(cx, &lit.token, lit.span, true);
61+
} else if matches!(lit.token.kind, LitKind::ByteStr) {
62+
check_lit(cx, &lit.token, lit.span, false);
63+
}
64+
}
65+
}
66+
}
67+
68+
fn check_lit(cx: &EarlyContext<'tcx>, lit: &Lit, span: Span, is_string: bool) {
69+
let contents = lit.symbol.as_str();
70+
let mut iter = contents.char_indices().peekable();
71+
let mut found = vec![];
72+
73+
// go through the string, looking for \0[0-7][0-7]?
74+
while let Some((from, ch)) = iter.next() {
75+
if ch == '\\' {
76+
if let Some((_, '0')) = iter.next() {
77+
// collect up to two further octal digits
78+
if let Some((mut to, '0'..='7')) = iter.next() {
79+
if let Some((_, '0'..='7')) = iter.peek() {
80+
to += 1;
81+
}
82+
found.push((from, to + 1));
83+
}
84+
}
85+
}
86+
}
87+
88+
if found.is_empty() {
89+
return;
90+
}
91+
92+
// construct two suggestion strings, one with \x escapes with octal meaning
93+
// as in C, and one with \x00 for null bytes.
94+
let mut suggest_1 = if is_string { "\"" } else { "b\"" }.to_string();
95+
let mut suggest_2 = suggest_1.clone();
96+
let mut index = 0;
97+
for (from, to) in found {
98+
suggest_1.push_str(&contents[index..from]);
99+
suggest_2.push_str(&contents[index..from]);
100+
101+
// construct a replacement escape
102+
// the maximum value is \077, or \x3f, so u8 is sufficient here
103+
if let Ok(n) = u8::from_str_radix(&contents[from + 1..to], 8) {
104+
write!(&mut suggest_1, "\\x{:02x}", n).unwrap();
105+
}
106+
107+
// append the null byte as \x00 and the following digits literally
108+
suggest_2.push_str("\\x00");
109+
suggest_2.push_str(&contents[from + 2..to]);
110+
111+
index = to;
112+
}
113+
suggest_1.push_str(&contents[index..]);
114+
suggest_1.push('"');
115+
suggest_2.push_str(&contents[index..]);
116+
suggest_2.push('"');
117+
118+
span_lint_and_then(
119+
cx,
120+
OCTAL_ESCAPES,
121+
span,
122+
&format!(
123+
"octal-looking escape in {} literal",
124+
if is_string { "string" } else { "byte string" }
125+
),
126+
|diag| {
127+
diag.help(&format!(
128+
"octal escapes are not supported, `\\0` is always a null {}",
129+
if is_string { "character" } else { "byte" }
130+
));
131+
// suggestion 1: equivalent hex escape
132+
diag.span_suggestion(
133+
span,
134+
"if an octal escape was intended, use the hexadecimal representation instead",
135+
suggest_1,
136+
Applicability::MaybeIncorrect,
137+
);
138+
// suggestion 2: unambiguous null byte
139+
diag.span_suggestion(
140+
span,
141+
&format!(
142+
"if the null {} is intended, disambiguate using",
143+
if is_string { "character" } else { "byte" }
144+
),
145+
suggest_2,
146+
Applicability::MaybeIncorrect,
147+
);
148+
},
149+
);
150+
}

tests/ui/octal_escapes.rs

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#![warn(clippy::octal_escapes)]
2+
3+
fn main() {
4+
let _bad1 = "\033[0m";
5+
let _bad2 = b"\033[0m";
6+
let _bad3 = "\\\033[0m";
7+
// maximum 3 digits (\012 is the escape)
8+
let _bad4 = "\01234567";
9+
let _bad5 = "\0\03";
10+
let _bad6 = "Text-\055\077-MoreText";
11+
let _bad7 = "EvenMoreText-\01\02-ShortEscapes";
12+
let _bad8 = "锈\01锈";
13+
let _bad9 = "锈\011锈";
14+
15+
let _good1 = "\\033[0m";
16+
let _good2 = "\0\\0";
17+
let _good3 = "\0\0";
18+
let _good4 = "X\0\0X";
19+
let _good5 = "锈\0锈";
20+
}

tests/ui/octal_escapes.stderr

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
error: octal-looking escape in string literal
2+
--> $DIR/octal_escapes.rs:4:17
3+
|
4+
LL | let _bad1 = "/033[0m";
5+
| ^^^^^^^^^
6+
|
7+
= note: `-D clippy::octal-escapes` implied by `-D warnings`
8+
= help: octal escapes are not supported, `/0` is always a null character
9+
help: if an octal escape was intended, use the hexadecimal representation instead
10+
|
11+
LL | let _bad1 = "/x1b[0m";
12+
| ~~~~~~~~~
13+
help: if the null character is intended, disambiguate using
14+
|
15+
LL | let _bad1 = "/x0033[0m";
16+
| ~~~~~~~~~~~
17+
18+
error: octal-looking escape in byte string literal
19+
--> $DIR/octal_escapes.rs:5:17
20+
|
21+
LL | let _bad2 = b"/033[0m";
22+
| ^^^^^^^^^^
23+
|
24+
= help: octal escapes are not supported, `/0` is always a null byte
25+
help: if an octal escape was intended, use the hexadecimal representation instead
26+
|
27+
LL | let _bad2 = b"/x1b[0m";
28+
| ~~~~~~~~~~
29+
help: if the null byte is intended, disambiguate using
30+
|
31+
LL | let _bad2 = b"/x0033[0m";
32+
| ~~~~~~~~~~~~
33+
34+
error: octal-looking escape in string literal
35+
--> $DIR/octal_escapes.rs:6:17
36+
|
37+
LL | let _bad3 = "//033[0m";
38+
| ^^^^^^^^^^^
39+
|
40+
= help: octal escapes are not supported, `/0` is always a null character
41+
help: if an octal escape was intended, use the hexadecimal representation instead
42+
|
43+
LL | let _bad3 = "//x1b[0m";
44+
| ~~~~~~~~~~~
45+
help: if the null character is intended, disambiguate using
46+
|
47+
LL | let _bad3 = "//x0033[0m";
48+
| ~~~~~~~~~~~~~
49+
50+
error: octal-looking escape in string literal
51+
--> $DIR/octal_escapes.rs:8:17
52+
|
53+
LL | let _bad4 = "/01234567";
54+
| ^^^^^^^^^^^
55+
|
56+
= help: octal escapes are not supported, `/0` is always a null character
57+
help: if an octal escape was intended, use the hexadecimal representation instead
58+
|
59+
LL | let _bad4 = "/x0a34567";
60+
| ~~~~~~~~~~~
61+
help: if the null character is intended, disambiguate using
62+
|
63+
LL | let _bad4 = "/x001234567";
64+
| ~~~~~~~~~~~~~
65+
66+
error: octal-looking escape in string literal
67+
--> $DIR/octal_escapes.rs:10:17
68+
|
69+
LL | let _bad6 = "Text-/055/077-MoreText";
70+
| ^^^^^^^^^^^^^^^^^^^^^^^^
71+
|
72+
= help: octal escapes are not supported, `/0` is always a null character
73+
help: if an octal escape was intended, use the hexadecimal representation instead
74+
|
75+
LL | let _bad6 = "Text-/x2d/x3f-MoreText";
76+
| ~~~~~~~~~~~~~~~~~~~~~~~~
77+
help: if the null character is intended, disambiguate using
78+
|
79+
LL | let _bad6 = "Text-/x0055/x0077-MoreText";
80+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
81+
82+
error: octal-looking escape in string literal
83+
--> $DIR/octal_escapes.rs:11:17
84+
|
85+
LL | let _bad7 = "EvenMoreText-/01/02-ShortEscapes";
86+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
87+
|
88+
= help: octal escapes are not supported, `/0` is always a null character
89+
help: if an octal escape was intended, use the hexadecimal representation instead
90+
|
91+
LL | let _bad7 = "EvenMoreText-/x01/x02-ShortEscapes";
92+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
93+
help: if the null character is intended, disambiguate using
94+
|
95+
LL | let _bad7 = "EvenMoreText-/x001/x002-ShortEscapes";
96+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
97+
98+
error: octal-looking escape in string literal
99+
--> $DIR/octal_escapes.rs:12:17
100+
|
101+
LL | let _bad8 = "锈/01锈";
102+
| ^^^^^^^^^
103+
|
104+
= help: octal escapes are not supported, `/0` is always a null character
105+
help: if an octal escape was intended, use the hexadecimal representation instead
106+
|
107+
LL | let _bad8 = "锈/x01锈";
108+
| ~~~~~~~~~~
109+
help: if the null character is intended, disambiguate using
110+
|
111+
LL | let _bad8 = "锈/x001锈";
112+
| ~~~~~~~~~~~
113+
114+
error: octal-looking escape in string literal
115+
--> $DIR/octal_escapes.rs:13:17
116+
|
117+
LL | let _bad9 = "锈/011锈";
118+
| ^^^^^^^^^^
119+
|
120+
= help: octal escapes are not supported, `/0` is always a null character
121+
help: if an octal escape was intended, use the hexadecimal representation instead
122+
|
123+
LL | let _bad9 = "锈/x09锈";
124+
| ~~~~~~~~~~
125+
help: if the null character is intended, disambiguate using
126+
|
127+
LL | let _bad9 = "锈/x0011锈";
128+
| ~~~~~~~~~~~~
129+
130+
error: aborting due to 8 previous errors
131+

0 commit comments

Comments
 (0)