]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/literal_representation.rs
Rollup merge of #92802 - compiler-errors:deduplicate-stack-trace, r=oli-obk
[rust.git] / src / tools / clippy / clippy_lints / src / literal_representation.rs
1 //! Lints concerned with the grouping of digits with underscores in integral or
2 //! floating-point literal expressions.
3
4 use clippy_utils::diagnostics::span_lint_and_sugg;
5 use clippy_utils::numeric_literal::{NumericLiteral, Radix};
6 use clippy_utils::source::snippet_opt;
7 use if_chain::if_chain;
8 use rustc_ast::ast::{Expr, ExprKind, Lit, LitKind};
9 use rustc_errors::Applicability;
10 use rustc_lint::{EarlyContext, EarlyLintPass, LintContext};
11 use rustc_middle::lint::in_external_macro;
12 use rustc_session::{declare_tool_lint, impl_lint_pass};
13 use std::iter;
14
15 declare_clippy_lint! {
16     /// ### What it does
17     /// Warns if a long integral or floating-point constant does
18     /// not contain underscores.
19     ///
20     /// ### Why is this bad?
21     /// Reading long numbers is difficult without separators.
22     ///
23     /// ### Example
24     /// ```rust
25     /// // Bad
26     /// let x: u64 = 61864918973511;
27     ///
28     /// // Good
29     /// let x: u64 = 61_864_918_973_511;
30     /// ```
31     #[clippy::version = "pre 1.29.0"]
32     pub UNREADABLE_LITERAL,
33     pedantic,
34     "long literal without underscores"
35 }
36
37 declare_clippy_lint! {
38     /// ### What it does
39     /// Warns for mistyped suffix in literals
40     ///
41     /// ### Why is this bad?
42     /// This is most probably a typo
43     ///
44     /// ### Known problems
45     /// - Recommends a signed suffix, even though the number might be too big and an unsigned
46     ///   suffix is required
47     /// - Does not match on `_127` since that is a valid grouping for decimal and octal numbers
48     ///
49     /// ### Example
50     /// ```rust
51     /// // Probably mistyped
52     /// 2_32;
53     ///
54     /// // Good
55     /// 2_i32;
56     /// ```
57     #[clippy::version = "1.30.0"]
58     pub MISTYPED_LITERAL_SUFFIXES,
59     correctness,
60     "mistyped literal suffix"
61 }
62
63 declare_clippy_lint! {
64     /// ### What it does
65     /// Warns if an integral or floating-point constant is
66     /// grouped inconsistently with underscores.
67     ///
68     /// ### Why is this bad?
69     /// Readers may incorrectly interpret inconsistently
70     /// grouped digits.
71     ///
72     /// ### Example
73     /// ```rust
74     /// // Bad
75     /// let x: u64 = 618_64_9189_73_511;
76     ///
77     /// // Good
78     /// let x: u64 = 61_864_918_973_511;
79     /// ```
80     #[clippy::version = "pre 1.29.0"]
81     pub INCONSISTENT_DIGIT_GROUPING,
82     style,
83     "integer literals with digits grouped inconsistently"
84 }
85
86 declare_clippy_lint! {
87     /// ### What it does
88     /// Warns if hexadecimal or binary literals are not grouped
89     /// by nibble or byte.
90     ///
91     /// ### Why is this bad?
92     /// Negatively impacts readability.
93     ///
94     /// ### Example
95     /// ```rust
96     /// let x: u32 = 0xFFF_FFF;
97     /// let y: u8 = 0b01_011_101;
98     /// ```
99     #[clippy::version = "1.49.0"]
100     pub UNUSUAL_BYTE_GROUPINGS,
101     style,
102     "binary or hex literals that aren't grouped by four"
103 }
104
105 declare_clippy_lint! {
106     /// ### What it does
107     /// Warns if the digits of an integral or floating-point
108     /// constant are grouped into groups that
109     /// are too large.
110     ///
111     /// ### Why is this bad?
112     /// Negatively impacts readability.
113     ///
114     /// ### Example
115     /// ```rust
116     /// let x: u64 = 6186491_8973511;
117     /// ```
118     #[clippy::version = "pre 1.29.0"]
119     pub LARGE_DIGIT_GROUPS,
120     pedantic,
121     "grouping digits into groups that are too large"
122 }
123
124 declare_clippy_lint! {
125     /// ### What it does
126     /// Warns if there is a better representation for a numeric literal.
127     ///
128     /// ### Why is this bad?
129     /// Especially for big powers of 2 a hexadecimal representation is more
130     /// readable than a decimal representation.
131     ///
132     /// ### Example
133     /// `255` => `0xFF`
134     /// `65_535` => `0xFFFF`
135     /// `4_042_322_160` => `0xF0F0_F0F0`
136     #[clippy::version = "pre 1.29.0"]
137     pub DECIMAL_LITERAL_REPRESENTATION,
138     restriction,
139     "using decimal representation when hexadecimal would be better"
140 }
141
142 enum WarningType {
143     UnreadableLiteral,
144     InconsistentDigitGrouping,
145     LargeDigitGroups,
146     DecimalRepresentation,
147     MistypedLiteralSuffix,
148     UnusualByteGroupings,
149 }
150
151 impl WarningType {
152     fn display(&self, suggested_format: String, cx: &EarlyContext<'_>, span: rustc_span::Span) {
153         match self {
154             Self::MistypedLiteralSuffix => span_lint_and_sugg(
155                 cx,
156                 MISTYPED_LITERAL_SUFFIXES,
157                 span,
158                 "mistyped literal suffix",
159                 "did you mean to write",
160                 suggested_format,
161                 Applicability::MaybeIncorrect,
162             ),
163             Self::UnreadableLiteral => span_lint_and_sugg(
164                 cx,
165                 UNREADABLE_LITERAL,
166                 span,
167                 "long literal lacking separators",
168                 "consider",
169                 suggested_format,
170                 Applicability::MachineApplicable,
171             ),
172             Self::LargeDigitGroups => span_lint_and_sugg(
173                 cx,
174                 LARGE_DIGIT_GROUPS,
175                 span,
176                 "digit groups should be smaller",
177                 "consider",
178                 suggested_format,
179                 Applicability::MachineApplicable,
180             ),
181             Self::InconsistentDigitGrouping => span_lint_and_sugg(
182                 cx,
183                 INCONSISTENT_DIGIT_GROUPING,
184                 span,
185                 "digits grouped inconsistently by underscores",
186                 "consider",
187                 suggested_format,
188                 Applicability::MachineApplicable,
189             ),
190             Self::DecimalRepresentation => span_lint_and_sugg(
191                 cx,
192                 DECIMAL_LITERAL_REPRESENTATION,
193                 span,
194                 "integer literal has a better hexadecimal representation",
195                 "consider",
196                 suggested_format,
197                 Applicability::MachineApplicable,
198             ),
199             Self::UnusualByteGroupings => span_lint_and_sugg(
200                 cx,
201                 UNUSUAL_BYTE_GROUPINGS,
202                 span,
203                 "digits of hex or binary literal not grouped by four",
204                 "consider",
205                 suggested_format,
206                 Applicability::MachineApplicable,
207             ),
208         };
209     }
210 }
211
212 #[allow(clippy::module_name_repetitions)]
213 #[derive(Copy, Clone)]
214 pub struct LiteralDigitGrouping {
215     lint_fraction_readability: bool,
216 }
217
218 impl_lint_pass!(LiteralDigitGrouping => [
219     UNREADABLE_LITERAL,
220     INCONSISTENT_DIGIT_GROUPING,
221     LARGE_DIGIT_GROUPS,
222     MISTYPED_LITERAL_SUFFIXES,
223     UNUSUAL_BYTE_GROUPINGS,
224 ]);
225
226 impl EarlyLintPass for LiteralDigitGrouping {
227     fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) {
228         if in_external_macro(cx.sess(), expr.span) {
229             return;
230         }
231
232         if let ExprKind::Lit(ref lit) = expr.kind {
233             self.check_lit(cx, lit);
234         }
235     }
236 }
237
238 // Length of each UUID hyphenated group in hex digits.
239 const UUID_GROUP_LENS: [usize; 5] = [8, 4, 4, 4, 12];
240
241 impl LiteralDigitGrouping {
242     pub fn new(lint_fraction_readability: bool) -> Self {
243         Self {
244             lint_fraction_readability,
245         }
246     }
247
248     fn check_lit(self, cx: &EarlyContext<'_>, lit: &Lit) {
249         if_chain! {
250             if let Some(src) = snippet_opt(cx, lit.span);
251             if let Some(mut num_lit) = NumericLiteral::from_lit(&src, lit);
252             then {
253                 if !Self::check_for_mistyped_suffix(cx, lit.span, &mut num_lit) {
254                     return;
255                 }
256
257                 if Self::is_literal_uuid_formatted(&mut num_lit) {
258                     return;
259                 }
260
261                 let result = (|| {
262
263                     let integral_group_size = Self::get_group_size(num_lit.integer.split('_'), num_lit.radix, true)?;
264                     if let Some(fraction) = num_lit.fraction {
265                         let fractional_group_size = Self::get_group_size(
266                             fraction.rsplit('_'),
267                             num_lit.radix,
268                             self.lint_fraction_readability)?;
269
270                         let consistent = Self::parts_consistent(integral_group_size,
271                                                                 fractional_group_size,
272                                                                 num_lit.integer.len(),
273                                                                 fraction.len());
274                         if !consistent {
275                             return Err(WarningType::InconsistentDigitGrouping);
276                         };
277                     }
278
279                     Ok(())
280                 })();
281
282
283                 if let Err(warning_type) = result {
284                     let should_warn = match warning_type {
285                         | WarningType::UnreadableLiteral
286                         | WarningType::InconsistentDigitGrouping
287                         | WarningType::UnusualByteGroupings
288                         | WarningType::LargeDigitGroups => {
289                             !lit.span.from_expansion()
290                         }
291                         WarningType::DecimalRepresentation | WarningType::MistypedLiteralSuffix => {
292                             true
293                         }
294                     };
295                     if should_warn {
296                         warning_type.display(num_lit.format(), cx, lit.span);
297                     }
298                 }
299             }
300         }
301     }
302
303     // Returns `false` if the check fails
304     fn check_for_mistyped_suffix(
305         cx: &EarlyContext<'_>,
306         span: rustc_span::Span,
307         num_lit: &mut NumericLiteral<'_>,
308     ) -> bool {
309         if num_lit.suffix.is_some() {
310             return true;
311         }
312
313         let (part, mistyped_suffixes, missing_char) = if let Some((_, exponent)) = &mut num_lit.exponent {
314             (exponent, &["32", "64"][..], 'f')
315         } else if num_lit.fraction.is_some() {
316             (&mut num_lit.integer, &["32", "64"][..], 'f')
317         } else {
318             (&mut num_lit.integer, &["8", "16", "32", "64"][..], 'i')
319         };
320
321         let mut split = part.rsplit('_');
322         let last_group = split.next().expect("At least one group");
323         if split.next().is_some() && mistyped_suffixes.contains(&last_group) {
324             *part = &part[..part.len() - last_group.len()];
325             let mut sugg = num_lit.format();
326             sugg.push('_');
327             sugg.push(missing_char);
328             sugg.push_str(last_group);
329             WarningType::MistypedLiteralSuffix.display(sugg, cx, span);
330             false
331         } else {
332             true
333         }
334     }
335
336     /// Checks whether the numeric literal matches the formatting of a UUID.
337     ///
338     /// Returns `true` if the radix is hexadecimal, and the groups match the
339     /// UUID format of 8-4-4-4-12.
340     fn is_literal_uuid_formatted(num_lit: &mut NumericLiteral<'_>) -> bool {
341         if num_lit.radix != Radix::Hexadecimal {
342             return false;
343         }
344
345         // UUIDs should not have a fraction
346         if num_lit.fraction.is_some() {
347             return false;
348         }
349
350         let group_sizes: Vec<usize> = num_lit.integer.split('_').map(str::len).collect();
351         if UUID_GROUP_LENS.len() == group_sizes.len() {
352             iter::zip(&UUID_GROUP_LENS, &group_sizes).all(|(&a, &b)| a == b)
353         } else {
354             false
355         }
356     }
357
358     /// Given the sizes of the digit groups of both integral and fractional
359     /// parts, and the length
360     /// of both parts, determine if the digits have been grouped consistently.
361     #[must_use]
362     fn parts_consistent(
363         int_group_size: Option<usize>,
364         frac_group_size: Option<usize>,
365         int_size: usize,
366         frac_size: usize,
367     ) -> bool {
368         match (int_group_size, frac_group_size) {
369             // No groups on either side of decimal point - trivially consistent.
370             (None, None) => true,
371             // Integral part has grouped digits, fractional part does not.
372             (Some(int_group_size), None) => frac_size <= int_group_size,
373             // Fractional part has grouped digits, integral part does not.
374             (None, Some(frac_group_size)) => int_size <= frac_group_size,
375             // Both parts have grouped digits. Groups should be the same size.
376             (Some(int_group_size), Some(frac_group_size)) => int_group_size == frac_group_size,
377         }
378     }
379
380     /// Returns the size of the digit groups (or None if ungrouped) if successful,
381     /// otherwise returns a `WarningType` for linting.
382     fn get_group_size<'a>(
383         groups: impl Iterator<Item = &'a str>,
384         radix: Radix,
385         lint_unreadable: bool,
386     ) -> Result<Option<usize>, WarningType> {
387         let mut groups = groups.map(str::len);
388
389         let first = groups.next().expect("At least one group");
390
391         if (radix == Radix::Binary || radix == Radix::Hexadecimal) && groups.any(|i| i != 4 && i != 2) {
392             return Err(WarningType::UnusualByteGroupings);
393         }
394
395         if let Some(second) = groups.next() {
396             if !groups.all(|x| x == second) || first > second {
397                 Err(WarningType::InconsistentDigitGrouping)
398             } else if second > 4 {
399                 Err(WarningType::LargeDigitGroups)
400             } else {
401                 Ok(Some(second))
402             }
403         } else if first > 5 && lint_unreadable {
404             Err(WarningType::UnreadableLiteral)
405         } else {
406             Ok(None)
407         }
408     }
409 }
410
411 #[allow(clippy::module_name_repetitions)]
412 #[derive(Copy, Clone)]
413 pub struct DecimalLiteralRepresentation {
414     threshold: u64,
415 }
416
417 impl_lint_pass!(DecimalLiteralRepresentation => [DECIMAL_LITERAL_REPRESENTATION]);
418
419 impl EarlyLintPass for DecimalLiteralRepresentation {
420     fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) {
421         if in_external_macro(cx.sess(), expr.span) {
422             return;
423         }
424
425         if let ExprKind::Lit(ref lit) = expr.kind {
426             self.check_lit(cx, lit);
427         }
428     }
429 }
430
431 impl DecimalLiteralRepresentation {
432     #[must_use]
433     pub fn new(threshold: u64) -> Self {
434         Self { threshold }
435     }
436     fn check_lit(self, cx: &EarlyContext<'_>, lit: &Lit) {
437         // Lint integral literals.
438         if_chain! {
439             if let LitKind::Int(val, _) = lit.kind;
440             if let Some(src) = snippet_opt(cx, lit.span);
441             if let Some(num_lit) = NumericLiteral::from_lit(&src, lit);
442             if num_lit.radix == Radix::Decimal;
443             if val >= u128::from(self.threshold);
444             then {
445                 let hex = format!("{:#X}", val);
446                 let num_lit = NumericLiteral::new(&hex, num_lit.suffix, false);
447                 let _ = Self::do_lint(num_lit.integer).map_err(|warning_type| {
448                     warning_type.display(num_lit.format(), cx, lit.span);
449                 });
450             }
451         }
452     }
453
454     fn do_lint(digits: &str) -> Result<(), WarningType> {
455         if digits.len() == 1 {
456             // Lint for 1 digit literals, if someone really sets the threshold that low
457             if digits == "1"
458                 || digits == "2"
459                 || digits == "4"
460                 || digits == "8"
461                 || digits == "3"
462                 || digits == "7"
463                 || digits == "F"
464             {
465                 return Err(WarningType::DecimalRepresentation);
466             }
467         } else if digits.len() < 4 {
468             // Lint for Literals with a hex-representation of 2 or 3 digits
469             let f = &digits[0..1]; // first digit
470             let s = &digits[1..]; // suffix
471
472             // Powers of 2
473             if ((f.eq("1") || f.eq("2") || f.eq("4") || f.eq("8")) && s.chars().all(|c| c == '0'))
474                 // Powers of 2 minus 1
475                 || ((f.eq("1") || f.eq("3") || f.eq("7") || f.eq("F")) && s.chars().all(|c| c == 'F'))
476             {
477                 return Err(WarningType::DecimalRepresentation);
478             }
479         } else {
480             // Lint for Literals with a hex-representation of 4 digits or more
481             let f = &digits[0..1]; // first digit
482             let m = &digits[1..digits.len() - 1]; // middle digits, except last
483             let s = &digits[1..]; // suffix
484
485             // Powers of 2 with a margin of +15/-16
486             if ((f.eq("1") || f.eq("2") || f.eq("4") || f.eq("8")) && m.chars().all(|c| c == '0'))
487                 || ((f.eq("1") || f.eq("3") || f.eq("7") || f.eq("F")) && m.chars().all(|c| c == 'F'))
488                 // Lint for representations with only 0s and Fs, while allowing 7 as the first
489                 // digit
490                 || ((f.eq("7") || f.eq("F")) && s.chars().all(|c| c == '0' || c == 'F'))
491             {
492                 return Err(WarningType::DecimalRepresentation);
493             }
494         }
495
496         Ok(())
497     }
498 }