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