]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/literal_digit_grouping.rs
Fix lines that exceed max width manually
[rust.git] / clippy_lints / src / literal_digit_grouping.rs
1 //! Lints concerned with the grouping of digits with underscores in integral or
2 //! floating-point literal expressions.
3
4 use rustc::lint::*;
5 use syntax::ast::*;
6 use syntax_pos;
7 use utils::{in_external_macro, snippet_opt, span_help_and_lint};
8
9 /// **What it does:** Warns if a long integral or floating-point constant does
10 /// not contain underscores.
11 ///
12 /// **Why is this bad?** Reading long numbers is difficult without separators.
13 ///
14 /// **Known problems:** None.
15 ///
16 /// **Example:**
17 ///
18 /// ```rust
19 /// 61864918973511
20 /// ```
21 declare_lint! {
22     pub UNREADABLE_LITERAL,
23     Warn,
24     "long integer literal without underscores"
25 }
26
27 /// **What it does:** Warns if an integral or floating-point constant is
28 /// grouped inconsistently with underscores.
29 ///
30 /// **Why is this bad?** Readers may incorrectly interpret inconsistently
31 /// grouped digits.
32 ///
33 /// **Known problems:** None.
34 ///
35 /// **Example:**
36 ///
37 /// ```rust
38 /// 618_64_9189_73_511
39 /// ```
40 declare_lint! {
41     pub INCONSISTENT_DIGIT_GROUPING,
42     Warn,
43     "integer literals with digits grouped inconsistently"
44 }
45
46 /// **What it does:** Warns if the digits of an integral or floating-point
47 /// constant are grouped into groups that
48 /// are too large.
49 ///
50 /// **Why is this bad?** Negatively impacts readability.
51 ///
52 /// **Known problems:** None.
53 ///
54 /// **Example:**
55 ///
56 /// ```rust
57 /// 6186491_8973511
58 /// ```
59 declare_lint! {
60     pub LARGE_DIGIT_GROUPS,
61     Warn,
62     "grouping digits into groups that are too large"
63 }
64
65 #[derive(Debug)]
66 enum Radix {
67     Binary,
68     Octal,
69     Decimal,
70     Hexadecimal,
71 }
72
73 impl Radix {
74     /// Return a reasonable digit group size for this radix.
75     pub fn suggest_grouping(&self) -> usize {
76         match *self {
77             Radix::Binary | Radix::Hexadecimal => 4,
78             Radix::Octal | Radix::Decimal => 3,
79         }
80     }
81 }
82
83 #[derive(Debug)]
84 struct DigitInfo<'a> {
85     /// Characters of a literal between the radix prefix and type suffix.
86     pub digits: &'a str,
87     /// Which radix the literal was represented in.
88     pub radix: Radix,
89     /// The radix prefix, if present.
90     pub prefix: Option<&'a str>,
91     /// The type suffix, including preceding underscore if present.
92     pub suffix: Option<&'a str>,
93     /// True for floating-point literals.
94     pub float: bool,
95 }
96
97 impl<'a> DigitInfo<'a> {
98     pub fn new(lit: &'a str, float: bool) -> Self {
99         // Determine delimiter for radix prefix, if present, and radix.
100         let radix = if lit.starts_with("0x") {
101             Radix::Hexadecimal
102         } else if lit.starts_with("0b") {
103             Radix::Binary
104         } else if lit.starts_with("0o") {
105             Radix::Octal
106         } else {
107             Radix::Decimal
108         };
109
110         // Grab part of the literal after prefix, if present.
111         let (prefix, sans_prefix) = if let Radix::Decimal = radix {
112             (None, lit)
113         } else {
114             let (p, s) = lit.split_at(2);
115             (Some(p), s)
116         };
117
118         let mut last_d = '\0';
119         for (d_idx, d) in sans_prefix.char_indices() {
120             if !float && (d == 'i' || d == 'u') || float && d == 'f' {
121                 let suffix_start = if last_d == '_' { d_idx - 1 } else { d_idx };
122                 let (digits, suffix) = sans_prefix.split_at(suffix_start);
123                 return Self {
124                     digits: digits,
125                     radix: radix,
126                     prefix: prefix,
127                     suffix: Some(suffix),
128                     float: float,
129                 };
130             }
131             last_d = d
132         }
133
134         // No suffix found
135         Self {
136             digits: sans_prefix,
137             radix: radix,
138             prefix: prefix,
139             suffix: None,
140             float: float,
141         }
142     }
143
144     /// Returns digits grouped in a sensible way.
145     fn grouping_hint(&self) -> String {
146         let group_size = self.radix.suggest_grouping();
147         if self.digits.contains('.') {
148             let mut parts = self.digits.split('.');
149             let int_part_hint = parts
150                 .next()
151                 .expect("split always returns at least one element")
152                 .chars()
153                 .rev()
154                 .filter(|&c| c != '_')
155                 .collect::<Vec<_>>()
156                 .chunks(group_size)
157                 .map(|chunk| chunk.into_iter().rev().collect())
158                 .rev()
159                 .collect::<Vec<String>>()
160                 .join("_");
161             let frac_part_hint = parts
162                 .next()
163                 .expect("already checked that there is a `.`")
164                 .chars()
165                 .filter(|&c| c != '_')
166                 .collect::<Vec<_>>()
167                 .chunks(group_size)
168                 .map(|chunk| chunk.into_iter().collect())
169                 .collect::<Vec<String>>()
170                 .join("_");
171             format!("{}.{}{}", int_part_hint, frac_part_hint, self.suffix.unwrap_or(""))
172         } else {
173             let hint = self.digits
174                 .chars()
175                 .rev()
176                 .filter(|&c| c != '_')
177                 .collect::<Vec<_>>()
178                 .chunks(group_size)
179                 .map(|chunk| chunk.into_iter().rev().collect())
180                 .rev()
181                 .collect::<Vec<String>>()
182                 .join("_");
183             format!("{}{}{}", self.prefix.unwrap_or(""), hint, self.suffix.unwrap_or(""))
184         }
185     }
186 }
187
188 enum WarningType {
189     UnreadableLiteral,
190     InconsistentDigitGrouping,
191     LargeDigitGroups,
192 }
193
194
195 impl WarningType {
196     pub fn display(&self, grouping_hint: &str, cx: &EarlyContext, span: &syntax_pos::Span) {
197         match *self {
198             WarningType::UnreadableLiteral => span_help_and_lint(
199                 cx,
200                 UNREADABLE_LITERAL,
201                 *span,
202                 "long literal lacking separators",
203                 &format!("consider: {}", grouping_hint),
204             ),
205             WarningType::LargeDigitGroups => span_help_and_lint(
206                 cx,
207                 LARGE_DIGIT_GROUPS,
208                 *span,
209                 "digit groups should be smaller",
210                 &format!("consider: {}", grouping_hint),
211             ),
212             WarningType::InconsistentDigitGrouping => span_help_and_lint(
213                 cx,
214                 INCONSISTENT_DIGIT_GROUPING,
215                 *span,
216                 "digits grouped inconsistently by underscores",
217                 &format!("consider: {}", grouping_hint),
218             ),
219         };
220     }
221 }
222
223 #[derive(Copy, Clone)]
224 pub struct LiteralDigitGrouping;
225
226 impl LintPass for LiteralDigitGrouping {
227     fn get_lints(&self) -> LintArray {
228         lint_array!(UNREADABLE_LITERAL, INCONSISTENT_DIGIT_GROUPING, LARGE_DIGIT_GROUPS)
229     }
230 }
231
232 impl EarlyLintPass for LiteralDigitGrouping {
233     fn check_expr(&mut self, cx: &EarlyContext, expr: &Expr) {
234         if in_external_macro(cx, expr.span) {
235             return;
236         }
237
238         if let ExprKind::Lit(ref lit) = expr.node {
239             self.check_lit(cx, lit)
240         }
241     }
242 }
243
244 impl LiteralDigitGrouping {
245     fn check_lit(&self, cx: &EarlyContext, lit: &Lit) {
246         // Lint integral literals.
247         if_chain! {
248             if let LitKind::Int(..) = lit.node;
249             if let Some(src) = snippet_opt(cx, lit.span);
250             if let Some(firstch) = src.chars().next();
251             if char::to_digit(firstch, 10).is_some();
252             then {
253                 let digit_info = DigitInfo::new(&src, false);
254                 let _ = Self::do_lint(digit_info.digits).map_err(|warning_type| {
255                     warning_type.display(&digit_info.grouping_hint(), cx, &lit.span)
256                 });
257             }
258         }
259
260         // Lint floating-point literals.
261         if_chain! {
262             if let LitKind::Float(..) = lit.node;
263             if let Some(src) = snippet_opt(cx, lit.span);
264             if let Some(firstch) = src.chars().next();
265             if char::to_digit(firstch, 10).is_some();
266             then {
267                 let digit_info = DigitInfo::new(&src, true);
268                 // Separate digits into integral and fractional parts.
269                 let parts: Vec<&str> = digit_info
270                     .digits
271                     .split_terminator('.')
272                     .collect();
273
274                 // Lint integral and fractional parts separately, and then check consistency of digit
275                 // groups if both pass.
276                 let _ = Self::do_lint(parts[0])
277                     .map(|integral_group_size| {
278                         if parts.len() > 1 {
279                             // Lint the fractional part of literal just like integral part, but reversed.
280                             let fractional_part = &parts[1].chars().rev().collect::<String>();
281                             let _ = Self::do_lint(fractional_part)
282                                 .map(|fractional_group_size| {
283                                     let consistent = Self::parts_consistent(integral_group_size,
284                                                                             fractional_group_size,
285                                                                             parts[0].len(),
286                                                                             parts[1].len());
287                                     if !consistent {
288                                         WarningType::InconsistentDigitGrouping.display(&digit_info.grouping_hint(),
289                                                                                        cx,
290                                                                                        &lit.span);
291                                     }
292                                 })
293                                 .map_err(|warning_type| warning_type.display(&digit_info.grouping_hint(),
294                                                                              cx,
295                                                                              &lit.span));
296                         }
297                     })
298                     .map_err(|warning_type| warning_type.display(&digit_info.grouping_hint(), cx, &lit.span));
299             }
300         }
301     }
302
303     /// Given the sizes of the digit groups of both integral and fractional
304     /// parts, and the length
305     /// of both parts, determine if the digits have been grouped consistently.
306     fn parts_consistent(int_group_size: usize, frac_group_size: usize, int_size: usize, frac_size: usize) -> bool {
307         match (int_group_size, frac_group_size) {
308             // No groups on either side of decimal point - trivially consistent.
309             (0, 0) => true,
310             // Integral part has grouped digits, fractional part does not.
311             (_, 0) => frac_size <= int_group_size,
312             // Fractional part has grouped digits, integral part does not.
313             (0, _) => int_size <= frac_group_size,
314             // Both parts have grouped digits. Groups should be the same size.
315             (_, _) => int_group_size == frac_group_size,
316         }
317     }
318
319     /// Performs lint on `digits` (no decimal point) and returns the group
320     /// size on success or `WarningType` when emitting a warning.
321     fn do_lint(digits: &str) -> Result<usize, WarningType> {
322         // Grab underscore indices with respect to the units digit.
323         let underscore_positions: Vec<usize> = digits
324             .chars()
325             .rev()
326             .enumerate()
327             .filter_map(|(idx, digit)| if digit == '_' { Some(idx) } else { None })
328             .collect();
329
330         if underscore_positions.is_empty() {
331             // Check if literal needs underscores.
332             if digits.len() > 4 {
333                 Err(WarningType::UnreadableLiteral)
334             } else {
335                 Ok(0)
336             }
337         } else {
338             // Check consistency and the sizes of the groups.
339             let group_size = underscore_positions[0];
340             let consistent = underscore_positions
341                 .windows(2)
342                 .all(|ps| ps[1] - ps[0] == group_size + 1)
343                 // number of digits to the left of the last group cannot be bigger than group size.
344                 && (digits.len() - underscore_positions.last()
345                                                        .expect("there's at least one element") <= group_size + 1);
346
347             if !consistent {
348                 return Err(WarningType::InconsistentDigitGrouping);
349             } else if group_size > 4 {
350                 return Err(WarningType::LargeDigitGroups);
351             }
352             Ok(group_size)
353         }
354     }
355 }