Skip to main content

rigsql_rules/aliasing/
al03.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// AL03: Expression aliases should have explicit AS keyword.
7///
8/// When a complex expression (not just a column reference) is aliased,
9/// the AS keyword should be present.
10#[derive(Debug, Default)]
11pub struct RuleAL03;
12
13impl Rule for RuleAL03 {
14    fn code(&self) -> &'static str {
15        "AL03"
16    }
17    fn name(&self) -> &'static str {
18        "aliasing.expression"
19    }
20    fn description(&self) -> &'static str {
21        "Column expression without alias. Use explicit alias."
22    }
23    fn explanation(&self) -> &'static str {
24        "Complex expressions in SELECT should have an explicit alias using AS. \
25         An unlabeled expression like 'SELECT a + b FROM t' is harder to work with \
26         than 'SELECT a + b AS total FROM t'. This makes result sets self-documenting."
27    }
28    fn groups(&self) -> &[RuleGroup] {
29        &[RuleGroup::Aliasing]
30    }
31    fn is_fixable(&self) -> bool {
32        false
33    }
34
35    fn crawl_type(&self) -> CrawlType {
36        CrawlType::Segment(vec![SegmentType::SelectClause])
37    }
38
39    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40        let children = ctx.segment.children();
41        let mut violations = Vec::new();
42
43        // Check each direct child of SelectClause
44        for child in children {
45            let st = child.segment_type();
46
47            // Skip trivia, keywords (SELECT, DISTINCT), commas
48            if st.is_trivia() || st == SegmentType::Keyword || st == SegmentType::Comma {
49                continue;
50            }
51
52            // If it's an expression (not column ref, not alias expr, not star),
53            // it should be aliased
54            if is_complex_expression(child) && !is_wrapped_in_alias(child, ctx) {
55                violations.push(LintViolation::with_msg_key(
56                    self.code(),
57                    "Column expression should have an explicit alias.",
58                    child.span(),
59                    "rules.AL03.msg",
60                    vec![],
61                ));
62            }
63        }
64
65        violations
66    }
67}
68
69fn is_complex_expression(seg: &Segment) -> bool {
70    matches!(
71        seg.segment_type(),
72        SegmentType::BinaryExpression
73            | SegmentType::FunctionCall
74            | SegmentType::CaseExpression
75            | SegmentType::CastExpression
76            | SegmentType::ParenExpression
77            | SegmentType::UnaryExpression
78    )
79}
80
81fn is_wrapped_in_alias(seg: &Segment, _ctx: &RuleContext) -> bool {
82    // If the segment is a direct child of SelectClause and it's a complex expression,
83    // check if there's an AliasExpression wrapping it.
84    // Actually, if the segment itself IS an alias expression, it's fine.
85    // The grammar wraps aliased items as AliasExpression, so if we see a bare expression,
86    // it means it wasn't aliased.
87    seg.segment_type() == SegmentType::AliasExpression
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::test_utils::lint_sql;
94
95    #[test]
96    fn test_al03_flags_function_without_alias() {
97        let violations = lint_sql("SELECT COUNT(*) FROM t", RuleAL03);
98        assert_eq!(violations.len(), 1);
99    }
100
101    #[test]
102    fn test_al03_accepts_function_with_alias() {
103        let violations = lint_sql("SELECT COUNT(*) AS cnt FROM t", RuleAL03);
104        assert_eq!(violations.len(), 0);
105    }
106
107    #[test]
108    fn test_al03_accepts_simple_column() {
109        let violations = lint_sql("SELECT a FROM t", RuleAL03);
110        assert_eq!(violations.len(), 0);
111    }
112}