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