]> git.lizzy.rs Git - rust.git/blob - compiler/rustc_lint/src/non_ascii_idents.rs
1cac1508bbd6b13bee9b5e7542f61d10f2c611a8
[rust.git] / compiler / rustc_lint / src / non_ascii_idents.rs
1 #![deny(rustc::untranslatable_diagnostic)]
2 #![deny(rustc::diagnostic_outside_of_impl)]
3 use crate::lints::{
4     ConfusableIdentifierPair, IdentifierNonAsciiChar, IdentifierUncommonCodepoints,
5     MixedScriptConfusables,
6 };
7 use crate::{EarlyContext, EarlyLintPass, LintContext};
8 use rustc_ast as ast;
9 use rustc_data_structures::fx::FxHashMap;
10 use rustc_span::symbol::Symbol;
11
12 declare_lint! {
13     /// The `non_ascii_idents` lint detects non-ASCII identifiers.
14     ///
15     /// ### Example
16     ///
17     /// ```rust,compile_fail
18     /// # #![allow(unused)]
19     /// #![deny(non_ascii_idents)]
20     /// fn main() {
21     ///     let föö = 1;
22     /// }
23     /// ```
24     ///
25     /// {{produces}}
26     ///
27     /// ### Explanation
28     ///
29     /// This lint allows projects that wish to retain the limit of only using
30     /// ASCII characters to switch this lint to "forbid" (for example to ease
31     /// collaboration or for security reasons).
32     /// See [RFC 2457] for more details.
33     ///
34     /// [RFC 2457]: https://github.com/rust-lang/rfcs/blob/master/text/2457-non-ascii-idents.md
35     pub NON_ASCII_IDENTS,
36     Allow,
37     "detects non-ASCII identifiers",
38     crate_level_only
39 }
40
41 declare_lint! {
42     /// The `uncommon_codepoints` lint detects uncommon Unicode codepoints in
43     /// identifiers.
44     ///
45     /// ### Example
46     ///
47     /// ```rust
48     /// # #![allow(unused)]
49     /// const µ: f64 = 0.000001;
50     /// ```
51     ///
52     /// {{produces}}
53     ///
54     /// ### Explanation
55     ///
56     /// This lint warns about using characters which are not commonly used, and may
57     /// cause visual confusion.
58     ///
59     /// This lint is triggered by identifiers that contain a codepoint that is
60     /// not part of the set of "Allowed" codepoints as described by [Unicode®
61     /// Technical Standard #39 Unicode Security Mechanisms Section 3.1 General
62     /// Security Profile for Identifiers][TR39Allowed].
63     ///
64     /// Note that the set of uncommon codepoints may change over time. Beware
65     /// that if you "forbid" this lint that existing code may fail in the
66     /// future.
67     ///
68     /// [TR39Allowed]: https://www.unicode.org/reports/tr39/#General_Security_Profile
69     pub UNCOMMON_CODEPOINTS,
70     Warn,
71     "detects uncommon Unicode codepoints in identifiers",
72     crate_level_only
73 }
74
75 declare_lint! {
76     /// The `confusable_idents` lint detects visually confusable pairs between
77     /// identifiers.
78     ///
79     /// ### Example
80     ///
81     /// ```rust
82     /// // Latin Capital Letter E With Caron
83     /// pub const Ě: i32 = 1;
84     /// // Latin Capital Letter E With Breve
85     /// pub const Ĕ: i32 = 2;
86     /// ```
87     ///
88     /// {{produces}}
89     ///
90     /// ### Explanation
91     ///
92     /// This lint warns when different identifiers may appear visually similar,
93     /// which can cause confusion.
94     ///
95     /// The confusable detection algorithm is based on [Unicode® Technical
96     /// Standard #39 Unicode Security Mechanisms Section 4 Confusable
97     /// Detection][TR39Confusable]. For every distinct identifier X execute
98     /// the function `skeleton(X)`. If there exist two distinct identifiers X
99     /// and Y in the same crate where `skeleton(X) = skeleton(Y)` report it.
100     /// The compiler uses the same mechanism to check if an identifier is too
101     /// similar to a keyword.
102     ///
103     /// Note that the set of confusable characters may change over time.
104     /// Beware that if you "forbid" this lint that existing code may fail in
105     /// the future.
106     ///
107     /// [TR39Confusable]: https://www.unicode.org/reports/tr39/#Confusable_Detection
108     pub CONFUSABLE_IDENTS,
109     Warn,
110     "detects visually confusable pairs between identifiers",
111     crate_level_only
112 }
113
114 declare_lint! {
115     /// The `mixed_script_confusables` lint detects visually confusable
116     /// characters in identifiers between different [scripts].
117     ///
118     /// [scripts]: https://en.wikipedia.org/wiki/Script_(Unicode)
119     ///
120     /// ### Example
121     ///
122     /// ```rust
123     /// // The Japanese katakana character エ can be confused with the Han character 工.
124     /// const エ: &'static str = "アイウ";
125     /// ```
126     ///
127     /// {{produces}}
128     ///
129     /// ### Explanation
130     ///
131     /// This lint warns when characters between different scripts may appear
132     /// visually similar, which can cause confusion.
133     ///
134     /// If the crate contains other identifiers in the same script that have
135     /// non-confusable characters, then this lint will *not* be issued. For
136     /// example, if the example given above has another identifier with
137     /// katakana characters (such as `let カタカナ = 123;`), then this indicates
138     /// that you are intentionally using katakana, and it will not warn about
139     /// it.
140     ///
141     /// Note that the set of confusable characters may change over time.
142     /// Beware that if you "forbid" this lint that existing code may fail in
143     /// the future.
144     pub MIXED_SCRIPT_CONFUSABLES,
145     Warn,
146     "detects Unicode scripts whose mixed script confusables codepoints are solely used",
147     crate_level_only
148 }
149
150 declare_lint_pass!(NonAsciiIdents => [NON_ASCII_IDENTS, UNCOMMON_CODEPOINTS, CONFUSABLE_IDENTS, MIXED_SCRIPT_CONFUSABLES]);
151
152 impl EarlyLintPass for NonAsciiIdents {
153     fn check_crate(&mut self, cx: &EarlyContext<'_>, _: &ast::Crate) {
154         use rustc_session::lint::Level;
155         use rustc_span::Span;
156         use std::collections::BTreeMap;
157         use unicode_security::GeneralSecurityProfile;
158
159         let check_non_ascii_idents = cx.builder.lint_level(NON_ASCII_IDENTS).0 != Level::Allow;
160         let check_uncommon_codepoints =
161             cx.builder.lint_level(UNCOMMON_CODEPOINTS).0 != Level::Allow;
162         let check_confusable_idents = cx.builder.lint_level(CONFUSABLE_IDENTS).0 != Level::Allow;
163         let check_mixed_script_confusables =
164             cx.builder.lint_level(MIXED_SCRIPT_CONFUSABLES).0 != Level::Allow;
165
166         if !check_non_ascii_idents
167             && !check_uncommon_codepoints
168             && !check_confusable_idents
169             && !check_mixed_script_confusables
170         {
171             return;
172         }
173
174         let mut has_non_ascii_idents = false;
175         let symbols = cx.sess().parse_sess.symbol_gallery.symbols.lock();
176
177         // Sort by `Span` so that error messages make sense with respect to the
178         // order of identifier locations in the code.
179         let mut symbols: Vec<_> = symbols.iter().collect();
180         symbols.sort_by_key(|k| k.1);
181
182         for (symbol, &sp) in symbols.iter() {
183             let symbol_str = symbol.as_str();
184             if symbol_str.is_ascii() {
185                 continue;
186             }
187             has_non_ascii_idents = true;
188             cx.emit_spanned_lint(NON_ASCII_IDENTS, sp, IdentifierNonAsciiChar);
189             if check_uncommon_codepoints
190                 && !symbol_str.chars().all(GeneralSecurityProfile::identifier_allowed)
191             {
192                 cx.emit_spanned_lint(UNCOMMON_CODEPOINTS, sp, IdentifierUncommonCodepoints);
193             }
194         }
195
196         if has_non_ascii_idents && check_confusable_idents {
197             let mut skeleton_map: FxHashMap<Symbol, (Symbol, Span, bool)> =
198                 FxHashMap::with_capacity_and_hasher(symbols.len(), Default::default());
199             let mut skeleton_buf = String::new();
200
201             for (&symbol, &sp) in symbols.iter() {
202                 use unicode_security::confusable_detection::skeleton;
203
204                 let symbol_str = symbol.as_str();
205                 let is_ascii = symbol_str.is_ascii();
206
207                 // Get the skeleton as a `Symbol`.
208                 skeleton_buf.clear();
209                 skeleton_buf.extend(skeleton(&symbol_str));
210                 let skeleton_sym = if *symbol_str == *skeleton_buf {
211                     symbol
212                 } else {
213                     Symbol::intern(&skeleton_buf)
214                 };
215
216                 skeleton_map
217                     .entry(skeleton_sym)
218                     .and_modify(|(existing_symbol, existing_span, existing_is_ascii)| {
219                         if !*existing_is_ascii || !is_ascii {
220                             cx.emit_spanned_lint(
221                                 CONFUSABLE_IDENTS,
222                                 sp,
223                                 ConfusableIdentifierPair {
224                                     existing_sym: *existing_symbol,
225                                     sym: symbol,
226                                     label: *existing_span,
227                                 },
228                             );
229                         }
230                         if *existing_is_ascii && !is_ascii {
231                             *existing_symbol = symbol;
232                             *existing_span = sp;
233                             *existing_is_ascii = is_ascii;
234                         }
235                     })
236                     .or_insert((symbol, sp, is_ascii));
237             }
238         }
239
240         if has_non_ascii_idents && check_mixed_script_confusables {
241             use unicode_security::is_potential_mixed_script_confusable_char;
242             use unicode_security::mixed_script::AugmentedScriptSet;
243
244             #[derive(Clone)]
245             enum ScriptSetUsage {
246                 Suspicious(Vec<char>, Span),
247                 Verified,
248             }
249
250             let mut script_states: FxHashMap<AugmentedScriptSet, ScriptSetUsage> =
251                 FxHashMap::default();
252             let latin_augmented_script_set = AugmentedScriptSet::for_char('A');
253             script_states.insert(latin_augmented_script_set, ScriptSetUsage::Verified);
254
255             let mut has_suspicous = false;
256             for (symbol, &sp) in symbols.iter() {
257                 let symbol_str = symbol.as_str();
258                 for ch in symbol_str.chars() {
259                     if ch.is_ascii() {
260                         // all ascii characters are covered by exception.
261                         continue;
262                     }
263                     if !GeneralSecurityProfile::identifier_allowed(ch) {
264                         // this character is covered by `uncommon_codepoints` lint.
265                         continue;
266                     }
267                     let augmented_script_set = AugmentedScriptSet::for_char(ch);
268                     script_states
269                         .entry(augmented_script_set)
270                         .and_modify(|existing_state| {
271                             if let ScriptSetUsage::Suspicious(ch_list, _) = existing_state {
272                                 if is_potential_mixed_script_confusable_char(ch) {
273                                     ch_list.push(ch);
274                                 } else {
275                                     *existing_state = ScriptSetUsage::Verified;
276                                 }
277                             }
278                         })
279                         .or_insert_with(|| {
280                             if !is_potential_mixed_script_confusable_char(ch) {
281                                 ScriptSetUsage::Verified
282                             } else {
283                                 has_suspicous = true;
284                                 ScriptSetUsage::Suspicious(vec![ch], sp)
285                             }
286                         });
287                 }
288             }
289
290             if has_suspicous {
291                 let verified_augmented_script_sets = script_states
292                     .iter()
293                     .flat_map(|(k, v)| match v {
294                         ScriptSetUsage::Verified => Some(*k),
295                         _ => None,
296                     })
297                     .collect::<Vec<_>>();
298
299                 // we're sorting the output here.
300                 let mut lint_reports: BTreeMap<(Span, Vec<char>), AugmentedScriptSet> =
301                     BTreeMap::new();
302
303                 'outerloop: for (augment_script_set, usage) in script_states {
304                     let ScriptSetUsage::Suspicious(mut ch_list, sp) = usage else { continue };
305
306                     if augment_script_set.is_all() {
307                         continue;
308                     }
309
310                     for existing in verified_augmented_script_sets.iter() {
311                         if existing.is_all() {
312                             continue;
313                         }
314                         let mut intersect = *existing;
315                         intersect.intersect_with(augment_script_set);
316                         if !intersect.is_empty() && !intersect.is_all() {
317                             continue 'outerloop;
318                         }
319                     }
320
321                     // We sort primitive chars here and can use unstable sort
322                     ch_list.sort_unstable();
323                     ch_list.dedup();
324                     lint_reports.insert((sp, ch_list), augment_script_set);
325                 }
326
327                 for ((sp, ch_list), script_set) in lint_reports {
328                     let mut includes = String::new();
329                     for (idx, ch) in ch_list.into_iter().enumerate() {
330                         if idx != 0 {
331                             includes += ", ";
332                         }
333                         let char_info = format!("'{}' (U+{:04X})", ch, ch as u32);
334                         includes += &char_info;
335                     }
336                     cx.emit_spanned_lint(
337                         MIXED_SCRIPT_CONFUSABLES,
338                         sp,
339                         MixedScriptConfusables { set: script_set.to_string(), includes },
340                     );
341                 }
342             }
343         }
344     }
345 }