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::new(
56                    self.code(),
57                    "Column expression should have an explicit alias.",
58                    child.span(),
59                ));
60            }
61        }
62
63        violations
64    }
65}
66
67fn is_complex_expression(seg: &Segment) -> bool {
68    matches!(
69        seg.segment_type(),
70        SegmentType::BinaryExpression
71            | SegmentType::FunctionCall
72            | SegmentType::CaseExpression
73            | SegmentType::CastExpression
74            | SegmentType::ParenExpression
75            | SegmentType::UnaryExpression
76    )
77}
78
79fn is_wrapped_in_alias(seg: &Segment, _ctx: &RuleContext) -> bool {
80    // If the segment is a direct child of SelectClause and it's a complex expression,
81    // check if there's an AliasExpression wrapping it.
82    // Actually, if the segment itself IS an alias expression, it's fine.
83    // The grammar wraps aliased items as AliasExpression, so if we see a bare expression,
84    // it means it wasn't aliased.
85    seg.segment_type() == SegmentType::AliasExpression
86}