sqrust_rules/convention/
leading_zero_numeric.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3use crate::capitalisation::SkipMap;
4
5pub struct LeadingZeroNumeric;
6
7const TRIGGER_CHARS: &[u8] = &[
10 b' ', b'\t', b'\n', b'\r', b'(', b')', b'[', b']', b'=', b'<', b'>', b'+', b'-', b'*', b'/', b',', b';', b'!', ];
17
18impl Rule for LeadingZeroNumeric {
19 fn name(&self) -> &'static str {
20 "Convention/LeadingZeroNumeric"
21 }
22
23 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
24 let source = &ctx.source;
25 let bytes = source.as_bytes();
26 let len = bytes.len();
27 let skip_map = SkipMap::build(source);
28
29 let mut diags = Vec::new();
30
31 for i in 0..len {
32 if bytes[i] != b'.' || !skip_map.is_code(i) {
34 continue;
35 }
36
37 if i + 1 >= len || !bytes[i + 1].is_ascii_digit() {
39 continue;
40 }
41
42 let preceded_by_trigger = if i == 0 {
47 true
48 } else {
49 let prev = bytes[i - 1];
50 TRIGGER_CHARS.contains(&prev)
51 };
52
53 if !preceded_by_trigger {
54 continue;
55 }
56
57 let (line, col) = line_col(source, i);
58 diags.push(Diagnostic {
59 rule: self.name(),
60 message: "Numeric literal missing leading zero (e.g. use 0.5 instead of .5)"
61 .to_string(),
62 line,
63 col,
64 });
65 }
66
67 diags
68 }
69}
70
71fn line_col(source: &str, offset: usize) -> (usize, usize) {
73 let before = &source[..offset];
74 let line = before.chars().filter(|&c| c == '\n').count() + 1;
75 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
76 (line, col)
77}