sqrust_rules/convention/
count_star.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3use crate::capitalisation::{is_word_char, SkipMap};
4
5pub struct CountStar;
6
7impl Rule for CountStar {
8 fn name(&self) -> &'static str {
9 "Convention/CountStar"
10 }
11
12 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
13 let source = &ctx.source;
14 let bytes = source.as_bytes();
15 let len = bytes.len();
16 let skip_map = SkipMap::build(source);
17
18 let mut diags = Vec::new();
19
20 let mut i = 0;
23 while i < len {
24 if bytes[i] != b'C' && bytes[i] != b'c' {
26 i += 1;
27 continue;
28 }
29
30 if i > 0 && is_word_char(bytes[i - 1]) {
32 i += 1;
33 continue;
34 }
35
36 if i + 7 >= len {
38 i += 1;
39 continue;
40 }
41
42 let is_count = bytes[i].eq_ignore_ascii_case(&b'C')
44 && bytes[i + 1].eq_ignore_ascii_case(&b'O')
45 && bytes[i + 2].eq_ignore_ascii_case(&b'U')
46 && bytes[i + 3].eq_ignore_ascii_case(&b'N')
47 && bytes[i + 4].eq_ignore_ascii_case(&b'T');
48 if !is_count {
49 i += 1;
50 continue;
51 }
52
53 if is_word_char(bytes[i + 5]) {
56 i += 1;
58 continue;
59 }
60
61 if bytes[i + 5] != b'(' {
63 i += 1;
64 continue;
65 }
66
67 if bytes[i + 6] != b'1' {
69 i += 1;
70 continue;
71 }
72
73 if bytes[i + 7] != b')' {
75 i += 1;
76 continue;
77 }
78
79 if !skip_map.is_code(i + 6) {
81 i += 1;
82 continue;
83 }
84
85 if !skip_map.is_code(i) {
87 i += 1;
88 continue;
89 }
90
91 let (line, col) = line_col(source, i);
92 diags.push(Diagnostic {
93 rule: self.name(),
94 message: "Use COUNT(*) instead of COUNT(1)".to_string(),
95 line,
96 col,
97 });
98
99 i += 8;
101 }
102
103 diags
104 }
105}
106
107fn line_col(source: &str, offset: usize) -> (usize, usize) {
109 let before = &source[..offset];
110 let line = before.chars().filter(|&c| c == '\n').count() + 1;
111 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
112 (line, col)
113}