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