]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/unicode.rs
Rollup merge of #105123 - BlackHoleFox:fixing-the-macos-deployment, r=oli-obk
[rust.git] / src / tools / clippy / clippy_lints / src / unicode.rs
1 use clippy_utils::diagnostics::span_lint_and_sugg;
2 use clippy_utils::is_lint_allowed;
3 use clippy_utils::macros::span_is_local;
4 use clippy_utils::source::snippet;
5 use rustc_ast::ast::LitKind;
6 use rustc_errors::Applicability;
7 use rustc_hir::{Expr, ExprKind, HirId};
8 use rustc_lint::{LateContext, LateLintPass};
9 use rustc_session::{declare_lint_pass, declare_tool_lint};
10 use rustc_span::source_map::Span;
11 use unicode_normalization::UnicodeNormalization;
12
13 declare_clippy_lint! {
14     /// ### What it does
15     /// Checks for invisible Unicode characters in the code.
16     ///
17     /// ### Why is this bad?
18     /// Having an invisible character in the code makes for all
19     /// sorts of April fools, but otherwise is very much frowned upon.
20     ///
21     /// ### Example
22     /// You don't see it, but there may be a zero-width space or soft hyphen
23     /// some­where in this text.
24     #[clippy::version = "1.49.0"]
25     pub INVISIBLE_CHARACTERS,
26     correctness,
27     "using an invisible character in a string literal, which is confusing"
28 }
29
30 declare_clippy_lint! {
31     /// ### What it does
32     /// Checks for non-ASCII characters in string and char literals.
33     ///
34     /// ### Why is this bad?
35     /// Yeah, we know, the 90's called and wanted their charset
36     /// back. Even so, there still are editors and other programs out there that
37     /// don't work well with Unicode. So if the code is meant to be used
38     /// internationally, on multiple operating systems, or has other portability
39     /// requirements, activating this lint could be useful.
40     ///
41     /// ### Example
42     /// ```rust
43     /// let x = String::from("€");
44     /// ```
45     ///
46     /// Use instead:
47     /// ```rust
48     /// let x = String::from("\u{20ac}");
49     /// ```
50     #[clippy::version = "pre 1.29.0"]
51     pub NON_ASCII_LITERAL,
52     restriction,
53     "using any literal non-ASCII chars in a string literal instead of using the `\\u` escape"
54 }
55
56 declare_clippy_lint! {
57     /// ### What it does
58     /// Checks for string literals that contain Unicode in a form
59     /// that is not equal to its
60     /// [NFC-recomposition](http://www.unicode.org/reports/tr15/#Norm_Forms).
61     ///
62     /// ### Why is this bad?
63     /// If such a string is compared to another, the results
64     /// may be surprising.
65     ///
66     /// ### Example
67     /// You may not see it, but "à"" and "à"" aren't the same string. The
68     /// former when escaped is actually `"a\u{300}"` while the latter is `"\u{e0}"`.
69     #[clippy::version = "pre 1.29.0"]
70     pub UNICODE_NOT_NFC,
71     pedantic,
72     "using a Unicode literal not in NFC normal form (see [Unicode tr15](http://www.unicode.org/reports/tr15/) for further information)"
73 }
74
75 declare_lint_pass!(Unicode => [INVISIBLE_CHARACTERS, NON_ASCII_LITERAL, UNICODE_NOT_NFC]);
76
77 impl LateLintPass<'_> for Unicode {
78     fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'_ Expr<'_>) {
79         if let ExprKind::Lit(ref lit) = expr.kind {
80             if let LitKind::Str(_, _) | LitKind::Char(_) = lit.node {
81                 check_str(cx, lit.span, expr.hir_id);
82             }
83         }
84     }
85 }
86
87 fn escape<T: Iterator<Item = char>>(s: T) -> String {
88     let mut result = String::new();
89     for c in s {
90         if c as u32 > 0x7F {
91             for d in c.escape_unicode() {
92                 result.push(d);
93             }
94         } else {
95             result.push(c);
96         }
97     }
98     result
99 }
100
101 fn check_str(cx: &LateContext<'_>, span: Span, id: HirId) {
102     if !span_is_local(span) {
103         return;
104     }
105
106     let string = snippet(cx, span, "");
107     if string.chars().any(|c| ['\u{200B}', '\u{ad}', '\u{2060}'].contains(&c)) {
108         span_lint_and_sugg(
109             cx,
110             INVISIBLE_CHARACTERS,
111             span,
112             "invisible character detected",
113             "consider replacing the string with",
114             string
115                 .replace('\u{200B}', "\\u{200B}")
116                 .replace('\u{ad}', "\\u{AD}")
117                 .replace('\u{2060}', "\\u{2060}"),
118             Applicability::MachineApplicable,
119         );
120     }
121
122     if string.chars().any(|c| c as u32 > 0x7F) {
123         span_lint_and_sugg(
124             cx,
125             NON_ASCII_LITERAL,
126             span,
127             "literal non-ASCII character detected",
128             "consider replacing the string with",
129             if is_lint_allowed(cx, UNICODE_NOT_NFC, id) {
130                 escape(string.chars())
131             } else {
132                 escape(string.nfc())
133             },
134             Applicability::MachineApplicable,
135         );
136     }
137
138     if is_lint_allowed(cx, NON_ASCII_LITERAL, id) && string.chars().zip(string.nfc()).any(|(a, b)| a != b) {
139         span_lint_and_sugg(
140             cx,
141             UNICODE_NOT_NFC,
142             span,
143             "non-NFC Unicode sequence detected",
144             "consider replacing the string with",
145             string.nfc().collect::<String>(),
146             Applicability::MachineApplicable,
147         );
148     }
149 }