]>
Commit | Line | Data |
---|---|---|
ea8adc8c XL |
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; | |
0531ce1d | 7 | use utils::{in_external_macro, snippet_opt, span_lint_and_sugg}; |
ea8adc8c XL |
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 | /// ``` | |
0531ce1d | 21 | declare_clippy_lint! { |
ea8adc8c | 22 | pub UNREADABLE_LITERAL, |
0531ce1d | 23 | style, |
ea8adc8c XL |
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 | /// ``` | |
0531ce1d | 40 | declare_clippy_lint! { |
ea8adc8c | 41 | pub INCONSISTENT_DIGIT_GROUPING, |
0531ce1d | 42 | style, |
ea8adc8c XL |
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 | /// ``` | |
0531ce1d | 59 | declare_clippy_lint! { |
ea8adc8c | 60 | pub LARGE_DIGIT_GROUPS, |
0531ce1d | 61 | style, |
ea8adc8c XL |
62 | "grouping digits into groups that are too large" |
63 | } | |
64 | ||
2c00a5a8 XL |
65 | /// **What it does:** Warns if there is a better representation for a numeric literal. |
66 | /// | |
67 | /// **Why is this bad?** Especially for big powers of 2 a hexadecimal representation is more | |
68 | /// readable than a decimal representation. | |
69 | /// | |
70 | /// **Known problems:** None. | |
71 | /// | |
72 | /// **Example:** | |
73 | /// | |
74 | /// `255` => `0xFF` | |
75 | /// `65_535` => `0xFFFF` | |
76 | /// `4_042_322_160` => `0xF0F0_F0F0` | |
0531ce1d | 77 | declare_clippy_lint! { |
2c00a5a8 | 78 | pub DECIMAL_LITERAL_REPRESENTATION, |
0531ce1d | 79 | restriction, |
2c00a5a8 XL |
80 | "using decimal representation when hexadecimal would be better" |
81 | } | |
82 | ||
83 | #[derive(Debug, PartialEq)] | |
94b46f34 | 84 | pub(super) enum Radix { |
ea8adc8c XL |
85 | Binary, |
86 | Octal, | |
87 | Decimal, | |
88 | Hexadecimal, | |
89 | } | |
90 | ||
91 | impl Radix { | |
92 | /// Return a reasonable digit group size for this radix. | |
93 | pub fn suggest_grouping(&self) -> usize { | |
94 | match *self { | |
95 | Radix::Binary | Radix::Hexadecimal => 4, | |
96 | Radix::Octal | Radix::Decimal => 3, | |
97 | } | |
98 | } | |
99 | } | |
100 | ||
101 | #[derive(Debug)] | |
94b46f34 | 102 | pub(super) struct DigitInfo<'a> { |
ea8adc8c XL |
103 | /// Characters of a literal between the radix prefix and type suffix. |
104 | pub digits: &'a str, | |
105 | /// Which radix the literal was represented in. | |
106 | pub radix: Radix, | |
107 | /// The radix prefix, if present. | |
108 | pub prefix: Option<&'a str>, | |
109 | /// The type suffix, including preceding underscore if present. | |
110 | pub suffix: Option<&'a str>, | |
111 | /// True for floating-point literals. | |
112 | pub float: bool, | |
113 | } | |
114 | ||
115 | impl<'a> DigitInfo<'a> { | |
116 | pub fn new(lit: &'a str, float: bool) -> Self { | |
117 | // Determine delimiter for radix prefix, if present, and radix. | |
118 | let radix = if lit.starts_with("0x") { | |
119 | Radix::Hexadecimal | |
120 | } else if lit.starts_with("0b") { | |
121 | Radix::Binary | |
122 | } else if lit.starts_with("0o") { | |
123 | Radix::Octal | |
124 | } else { | |
125 | Radix::Decimal | |
126 | }; | |
127 | ||
128 | // Grab part of the literal after prefix, if present. | |
129 | let (prefix, sans_prefix) = if let Radix::Decimal = radix { | |
130 | (None, lit) | |
131 | } else { | |
132 | let (p, s) = lit.split_at(2); | |
133 | (Some(p), s) | |
134 | }; | |
135 | ||
136 | let mut last_d = '\0'; | |
137 | for (d_idx, d) in sans_prefix.char_indices() { | |
0531ce1d | 138 | if !float && (d == 'i' || d == 'u') || float && (d == 'f' || d == 'e' || d == 'E') { |
ea8adc8c XL |
139 | let suffix_start = if last_d == '_' { d_idx - 1 } else { d_idx }; |
140 | let (digits, suffix) = sans_prefix.split_at(suffix_start); | |
141 | return Self { | |
0531ce1d XL |
142 | digits, |
143 | radix, | |
144 | prefix, | |
ea8adc8c | 145 | suffix: Some(suffix), |
0531ce1d | 146 | float, |
ea8adc8c XL |
147 | }; |
148 | } | |
149 | last_d = d | |
150 | } | |
151 | ||
152 | // No suffix found | |
153 | Self { | |
154 | digits: sans_prefix, | |
0531ce1d XL |
155 | radix, |
156 | prefix, | |
ea8adc8c | 157 | suffix: None, |
0531ce1d | 158 | float, |
ea8adc8c XL |
159 | } |
160 | } | |
161 | ||
162 | /// Returns digits grouped in a sensible way. | |
94b46f34 | 163 | pub fn grouping_hint(&self) -> String { |
ea8adc8c XL |
164 | let group_size = self.radix.suggest_grouping(); |
165 | if self.digits.contains('.') { | |
166 | let mut parts = self.digits.split('.'); | |
167 | let int_part_hint = parts | |
168 | .next() | |
169 | .expect("split always returns at least one element") | |
170 | .chars() | |
171 | .rev() | |
172 | .filter(|&c| c != '_') | |
173 | .collect::<Vec<_>>() | |
174 | .chunks(group_size) | |
175 | .map(|chunk| chunk.into_iter().rev().collect()) | |
176 | .rev() | |
177 | .collect::<Vec<String>>() | |
178 | .join("_"); | |
179 | let frac_part_hint = parts | |
180 | .next() | |
181 | .expect("already checked that there is a `.`") | |
182 | .chars() | |
183 | .filter(|&c| c != '_') | |
184 | .collect::<Vec<_>>() | |
185 | .chunks(group_size) | |
186 | .map(|chunk| chunk.into_iter().collect()) | |
187 | .collect::<Vec<String>>() | |
188 | .join("_"); | |
2c00a5a8 XL |
189 | format!( |
190 | "{}.{}{}", | |
191 | int_part_hint, | |
192 | frac_part_hint, | |
193 | self.suffix.unwrap_or("") | |
194 | ) | |
ea8adc8c XL |
195 | } else { |
196 | let hint = self.digits | |
197 | .chars() | |
198 | .rev() | |
199 | .filter(|&c| c != '_') | |
200 | .collect::<Vec<_>>() | |
201 | .chunks(group_size) | |
202 | .map(|chunk| chunk.into_iter().rev().collect()) | |
203 | .rev() | |
204 | .collect::<Vec<String>>() | |
205 | .join("_"); | |
2c00a5a8 XL |
206 | format!( |
207 | "{}{}{}", | |
208 | self.prefix.unwrap_or(""), | |
209 | hint, | |
210 | self.suffix.unwrap_or("") | |
211 | ) | |
ea8adc8c XL |
212 | } |
213 | } | |
214 | } | |
215 | ||
216 | enum WarningType { | |
217 | UnreadableLiteral, | |
218 | InconsistentDigitGrouping, | |
219 | LargeDigitGroups, | |
2c00a5a8 | 220 | DecimalRepresentation, |
ea8adc8c XL |
221 | } |
222 | ||
ea8adc8c XL |
223 | impl WarningType { |
224 | pub fn display(&self, grouping_hint: &str, cx: &EarlyContext, span: &syntax_pos::Span) { | |
225 | match *self { | |
0531ce1d | 226 | WarningType::UnreadableLiteral => span_lint_and_sugg( |
abe05a73 XL |
227 | cx, |
228 | UNREADABLE_LITERAL, | |
229 | *span, | |
230 | "long literal lacking separators", | |
0531ce1d XL |
231 | "consider", |
232 | grouping_hint.to_owned(), | |
abe05a73 | 233 | ), |
0531ce1d | 234 | WarningType::LargeDigitGroups => span_lint_and_sugg( |
abe05a73 XL |
235 | cx, |
236 | LARGE_DIGIT_GROUPS, | |
237 | *span, | |
238 | "digit groups should be smaller", | |
0531ce1d XL |
239 | "consider", |
240 | grouping_hint.to_owned(), | |
abe05a73 | 241 | ), |
0531ce1d | 242 | WarningType::InconsistentDigitGrouping => span_lint_and_sugg( |
abe05a73 XL |
243 | cx, |
244 | INCONSISTENT_DIGIT_GROUPING, | |
245 | *span, | |
246 | "digits grouped inconsistently by underscores", | |
0531ce1d XL |
247 | "consider", |
248 | grouping_hint.to_owned(), | |
abe05a73 | 249 | ), |
0531ce1d | 250 | WarningType::DecimalRepresentation => span_lint_and_sugg( |
2c00a5a8 XL |
251 | cx, |
252 | DECIMAL_LITERAL_REPRESENTATION, | |
253 | *span, | |
254 | "integer literal has a better hexadecimal representation", | |
0531ce1d XL |
255 | "consider", |
256 | grouping_hint.to_owned(), | |
2c00a5a8 | 257 | ), |
ea8adc8c XL |
258 | }; |
259 | } | |
260 | } | |
261 | ||
262 | #[derive(Copy, Clone)] | |
263 | pub struct LiteralDigitGrouping; | |
264 | ||
265 | impl LintPass for LiteralDigitGrouping { | |
266 | fn get_lints(&self) -> LintArray { | |
2c00a5a8 XL |
267 | lint_array!( |
268 | UNREADABLE_LITERAL, | |
269 | INCONSISTENT_DIGIT_GROUPING, | |
270 | LARGE_DIGIT_GROUPS | |
271 | ) | |
ea8adc8c XL |
272 | } |
273 | } | |
274 | ||
275 | impl EarlyLintPass for LiteralDigitGrouping { | |
276 | fn check_expr(&mut self, cx: &EarlyContext, expr: &Expr) { | |
277 | if in_external_macro(cx, expr.span) { | |
278 | return; | |
279 | } | |
280 | ||
281 | if let ExprKind::Lit(ref lit) = expr.node { | |
282 | self.check_lit(cx, lit) | |
283 | } | |
284 | } | |
285 | } | |
286 | ||
287 | impl LiteralDigitGrouping { | |
288 | fn check_lit(&self, cx: &EarlyContext, lit: &Lit) { | |
0531ce1d XL |
289 | match lit.node { |
290 | LitKind::Int(..) => { | |
291 | // Lint integral literals. | |
292 | if_chain! { | |
293 | if let Some(src) = snippet_opt(cx, lit.span); | |
294 | if let Some(firstch) = src.chars().next(); | |
295 | if char::to_digit(firstch, 10).is_some(); | |
296 | then { | |
297 | let digit_info = DigitInfo::new(&src, false); | |
298 | let _ = Self::do_lint(digit_info.digits).map_err(|warning_type| { | |
299 | warning_type.display(&digit_info.grouping_hint(), cx, &lit.span) | |
300 | }); | |
301 | } | |
302 | } | |
303 | }, | |
304 | LitKind::Float(..) | LitKind::FloatUnsuffixed(..) => { | |
305 | // Lint floating-point literals. | |
306 | if_chain! { | |
307 | if let Some(src) = snippet_opt(cx, lit.span); | |
308 | if let Some(firstch) = src.chars().next(); | |
309 | if char::to_digit(firstch, 10).is_some(); | |
310 | then { | |
311 | let digit_info = DigitInfo::new(&src, true); | |
312 | // Separate digits into integral and fractional parts. | |
313 | let parts: Vec<&str> = digit_info | |
314 | .digits | |
315 | .split_terminator('.') | |
316 | .collect(); | |
ea8adc8c | 317 | |
0531ce1d XL |
318 | // Lint integral and fractional parts separately, and then check consistency of digit |
319 | // groups if both pass. | |
320 | let _ = Self::do_lint(parts[0]) | |
321 | .map(|integral_group_size| { | |
322 | if parts.len() > 1 { | |
323 | // Lint the fractional part of literal just like integral part, but reversed. | |
324 | let fractional_part = &parts[1].chars().rev().collect::<String>(); | |
325 | let _ = Self::do_lint(fractional_part) | |
326 | .map(|fractional_group_size| { | |
327 | let consistent = Self::parts_consistent(integral_group_size, | |
328 | fractional_group_size, | |
329 | parts[0].len(), | |
330 | parts[1].len()); | |
331 | if !consistent { | |
332 | WarningType::InconsistentDigitGrouping.display(&digit_info.grouping_hint(), | |
333 | cx, | |
334 | &lit.span); | |
335 | } | |
336 | }) | |
337 | .map_err(|warning_type| warning_type.display(&digit_info.grouping_hint(), | |
338 | cx, | |
339 | &lit.span)); | |
340 | } | |
341 | }) | |
342 | .map_err(|warning_type| warning_type.display(&digit_info.grouping_hint(), cx, &lit.span)); | |
343 | } | |
344 | } | |
345 | }, | |
346 | _ => (), | |
abe05a73 | 347 | } |
ea8adc8c XL |
348 | } |
349 | ||
350 | /// Given the sizes of the digit groups of both integral and fractional | |
351 | /// parts, and the length | |
352 | /// of both parts, determine if the digits have been grouped consistently. | |
353 | fn parts_consistent(int_group_size: usize, frac_group_size: usize, int_size: usize, frac_size: usize) -> bool { | |
354 | match (int_group_size, frac_group_size) { | |
355 | // No groups on either side of decimal point - trivially consistent. | |
356 | (0, 0) => true, | |
357 | // Integral part has grouped digits, fractional part does not. | |
358 | (_, 0) => frac_size <= int_group_size, | |
359 | // Fractional part has grouped digits, integral part does not. | |
360 | (0, _) => int_size <= frac_group_size, | |
361 | // Both parts have grouped digits. Groups should be the same size. | |
362 | (_, _) => int_group_size == frac_group_size, | |
363 | } | |
364 | } | |
365 | ||
366 | /// Performs lint on `digits` (no decimal point) and returns the group | |
367 | /// size on success or `WarningType` when emitting a warning. | |
368 | fn do_lint(digits: &str) -> Result<usize, WarningType> { | |
369 | // Grab underscore indices with respect to the units digit. | |
370 | let underscore_positions: Vec<usize> = digits | |
371 | .chars() | |
372 | .rev() | |
373 | .enumerate() | |
374 | .filter_map(|(idx, digit)| if digit == '_' { Some(idx) } else { None }) | |
375 | .collect(); | |
376 | ||
377 | if underscore_positions.is_empty() { | |
378 | // Check if literal needs underscores. | |
0531ce1d | 379 | if digits.len() > 5 { |
ea8adc8c XL |
380 | Err(WarningType::UnreadableLiteral) |
381 | } else { | |
382 | Ok(0) | |
383 | } | |
384 | } else { | |
385 | // Check consistency and the sizes of the groups. | |
386 | let group_size = underscore_positions[0]; | |
387 | let consistent = underscore_positions | |
388 | .windows(2) | |
389 | .all(|ps| ps[1] - ps[0] == group_size + 1) | |
390 | // number of digits to the left of the last group cannot be bigger than group size. | |
abe05a73 XL |
391 | && (digits.len() - underscore_positions.last() |
392 | .expect("there's at least one element") <= group_size + 1); | |
ea8adc8c XL |
393 | |
394 | if !consistent { | |
395 | return Err(WarningType::InconsistentDigitGrouping); | |
396 | } else if group_size > 4 { | |
397 | return Err(WarningType::LargeDigitGroups); | |
398 | } | |
399 | Ok(group_size) | |
400 | } | |
401 | } | |
402 | } | |
2c00a5a8 XL |
403 | |
404 | #[derive(Copy, Clone)] | |
405 | pub struct LiteralRepresentation { | |
406 | threshold: u64, | |
407 | } | |
408 | ||
409 | impl LintPass for LiteralRepresentation { | |
410 | fn get_lints(&self) -> LintArray { | |
411 | lint_array!(DECIMAL_LITERAL_REPRESENTATION) | |
412 | } | |
413 | } | |
414 | ||
415 | impl EarlyLintPass for LiteralRepresentation { | |
416 | fn check_expr(&mut self, cx: &EarlyContext, expr: &Expr) { | |
417 | if in_external_macro(cx, expr.span) { | |
418 | return; | |
419 | } | |
420 | ||
421 | if let ExprKind::Lit(ref lit) = expr.node { | |
422 | self.check_lit(cx, lit) | |
423 | } | |
424 | } | |
425 | } | |
426 | ||
427 | impl LiteralRepresentation { | |
428 | pub fn new(threshold: u64) -> Self { | |
429 | Self { | |
0531ce1d | 430 | threshold, |
2c00a5a8 XL |
431 | } |
432 | } | |
433 | fn check_lit(&self, cx: &EarlyContext, lit: &Lit) { | |
434 | // Lint integral literals. | |
435 | if_chain! { | |
436 | if let LitKind::Int(..) = lit.node; | |
437 | if let Some(src) = snippet_opt(cx, lit.span); | |
438 | if let Some(firstch) = src.chars().next(); | |
439 | if char::to_digit(firstch, 10).is_some(); | |
440 | then { | |
441 | let digit_info = DigitInfo::new(&src, false); | |
442 | if digit_info.radix == Radix::Decimal { | |
443 | let val = digit_info.digits | |
444 | .chars() | |
445 | .filter(|&c| c != '_') | |
446 | .collect::<String>() | |
447 | .parse::<u128>().unwrap(); | |
0531ce1d | 448 | if val < u128::from(self.threshold) { |
2c00a5a8 XL |
449 | return |
450 | } | |
451 | let hex = format!("{:#X}", val); | |
452 | let digit_info = DigitInfo::new(&hex[..], false); | |
453 | let _ = Self::do_lint(digit_info.digits).map_err(|warning_type| { | |
454 | warning_type.display(&digit_info.grouping_hint(), cx, &lit.span) | |
455 | }); | |
456 | } | |
457 | } | |
458 | } | |
459 | } | |
460 | ||
461 | fn do_lint(digits: &str) -> Result<(), WarningType> { | |
462 | if digits.len() == 1 { | |
463 | // Lint for 1 digit literals, if someone really sets the threshold that low | |
464 | if digits == "1" || digits == "2" || digits == "4" || digits == "8" || digits == "3" || digits == "7" | |
465 | || digits == "F" | |
466 | { | |
467 | return Err(WarningType::DecimalRepresentation); | |
468 | } | |
469 | } else if digits.len() < 4 { | |
470 | // Lint for Literals with a hex-representation of 2 or 3 digits | |
471 | let f = &digits[0..1]; // first digit | |
472 | let s = &digits[1..]; // suffix | |
473 | // Powers of 2 | |
474 | if ((f.eq("1") || f.eq("2") || f.eq("4") || f.eq("8")) && s.chars().all(|c| c == '0')) | |
475 | // Powers of 2 minus 1 | |
476 | || ((f.eq("1") || f.eq("3") || f.eq("7") || f.eq("F")) && s.chars().all(|c| c == 'F')) | |
477 | { | |
478 | return Err(WarningType::DecimalRepresentation); | |
479 | } | |
480 | } else { | |
481 | // Lint for Literals with a hex-representation of 4 digits or more | |
482 | let f = &digits[0..1]; // first digit | |
483 | let m = &digits[1..digits.len() - 1]; // middle digits, except last | |
484 | let s = &digits[1..]; // suffix | |
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 | } |