]>
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; | |
abe05a73 | 7 | use utils::{in_external_macro, snippet_opt, span_help_and_lint}; |
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 | /// ``` | |
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 { | |
abe05a73 XL |
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 | ), | |
ea8adc8c XL |
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. | |
abe05a73 XL |
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 | } | |
ea8adc8c XL |
259 | |
260 | // Lint floating-point literals. | |
abe05a73 XL |
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(); | |
ea8adc8c | 273 | |
abe05a73 XL |
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 | } | |
ea8adc8c XL |
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. | |
abe05a73 XL |
344 | && (digits.len() - underscore_positions.last() |
345 | .expect("there's at least one element") <= group_size + 1); | |
ea8adc8c XL |
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 | } |