Skip to main content

sqrust_rules/convention/
select_star.rs

1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3use crate::capitalisation::SkipMap;
4
5pub struct SelectStar;
6
7impl Rule for SelectStar {
8    fn name(&self) -> &'static str {
9        "Convention/SelectStar"
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        for i in 0..len {
21            if bytes[i] != b'*' {
22                continue;
23            }
24            if !skip_map.is_code(i) {
25                continue;
26            }
27
28            // Case 1: qualified wildcard — immediately preceded by '.'
29            // e.g. `t.*`
30            if i > 0 && bytes[i - 1] == b'.' {
31                let (line, col) = line_col(source, i);
32                diags.push(Diagnostic {
33                    rule: self.name(),
34                    message: "Avoid SELECT *; list columns explicitly".to_string(),
35                    line,
36                    col,
37                });
38                continue;
39            }
40
41            // Case 2: standalone wildcard — preceded by whitespace AND not
42            // preceded by '(' (which would make it COUNT(*) style).
43            // The '*' must also be followed by whitespace, ',', ';', or EOF.
44            if i > 0 {
45                let prev = bytes[i - 1];
46                if prev == b'(' {
47                    // Inside a function call: COUNT(*), SUM(*), etc. — skip.
48                    continue;
49                }
50                if prev == b' ' || prev == b'\t' || prev == b'\n' || prev == b'\r' {
51                    // Check what follows the '*'
52                    let next = if i + 1 < len { bytes[i + 1] } else { 0 };
53                    let followed_by_separator = next == b' '
54                        || next == b'\t'
55                        || next == b'\n'
56                        || next == b'\r'
57                        || next == b','
58                        || next == b';'
59                        || next == 0;
60                    if followed_by_separator {
61                        let (line, col) = line_col(source, i);
62                        diags.push(Diagnostic {
63                            rule: self.name(),
64                            message: "Avoid SELECT *; list columns explicitly".to_string(),
65                            line,
66                            col,
67                        });
68                    }
69                }
70            }
71        }
72
73        diags
74    }
75}
76
77/// Converts a byte offset in `source` to a 1-indexed (line, col) pair.
78fn line_col(source: &str, offset: usize) -> (usize, usize) {
79    let before = &source[..offset];
80    let line = before.chars().filter(|&c| c == '\n').count() + 1;
81    let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
82    (line, col)
83}