Skip to main content

rigsql_rules/references/
rf03.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// RF03: Column qualification should be consistent (all qualified or all unqualified).
7///
8/// In a SELECT statement, if some column references are qualified and some are not,
9/// flag the inconsistency.
10#[derive(Debug, Default)]
11pub struct RuleRF03;
12
13impl Rule for RuleRF03 {
14    fn code(&self) -> &'static str {
15        "RF03"
16    }
17    fn name(&self) -> &'static str {
18        "references.consistent"
19    }
20    fn description(&self) -> &'static str {
21        "Column qualification should be consistent."
22    }
23    fn explanation(&self) -> &'static str {
24        "Within a single SELECT statement, column references should be consistently \
25         qualified or unqualified. Mixing styles (e.g., 'users.id' alongside bare 'name') \
26         reduces readability and can indicate accidental omissions."
27    }
28    fn groups(&self) -> &[RuleGroup] {
29        &[RuleGroup::References]
30    }
31    fn is_fixable(&self) -> bool {
32        false
33    }
34
35    fn crawl_type(&self) -> CrawlType {
36        CrawlType::Segment(vec![SegmentType::SelectStatement])
37    }
38
39    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
40        let mut qualified_count = 0usize;
41        let mut unqualified: Vec<rigsql_core::Span> = Vec::new();
42
43        // Look at the SelectClause children to find column references
44        for child in ctx.segment.children() {
45            if child.segment_type() == SegmentType::SelectClause {
46                for sel_child in child.children() {
47                    match sel_child.segment_type() {
48                        // Bare Identifier = unqualified column reference
49                        SegmentType::Identifier => {
50                            if let Segment::Token(t) = sel_child {
51                                unqualified.push(t.token.span);
52                            }
53                        }
54                        // ColumnRef = qualified column reference (e.g., u.id)
55                        SegmentType::ColumnRef => {
56                            qualified_count += 1;
57                        }
58                        // AliasExpression may contain either
59                        SegmentType::AliasExpression => {
60                            // Check the first non-trivia child of the alias expression
61                            for alias_child in sel_child.children() {
62                                let st = alias_child.segment_type();
63                                if st.is_trivia()
64                                    || st == SegmentType::Keyword
65                                    || st == SegmentType::Comma
66                                {
67                                    continue;
68                                }
69                                if st == SegmentType::ColumnRef {
70                                    qualified_count += 1;
71                                } else if st == SegmentType::Identifier {
72                                    if let Segment::Token(t) = alias_child {
73                                        unqualified.push(t.token.span);
74                                    }
75                                }
76                                break; // Only check the first meaningful child
77                            }
78                        }
79                        _ => {}
80                    }
81                }
82            }
83        }
84
85        // Only flag if there is a mix of both styles
86        if qualified_count == 0 || unqualified.is_empty() {
87            return vec![];
88        }
89
90        unqualified
91            .iter()
92            .map(|span| {
93                LintViolation::new(
94                    self.code(),
95                    "Inconsistent column qualification. Mix of qualified and unqualified references."
96                        .to_string(),
97                    *span,
98                )
99            })
100            .collect()
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::test_utils::lint_sql;
108
109    #[test]
110    fn test_rf03_flags_inconsistent_qualification() {
111        let violations = lint_sql(
112            "SELECT u.id, name FROM users u JOIN orders o ON u.id = o.user_id",
113            RuleRF03,
114        );
115        assert!(
116            !violations.is_empty(),
117            "Should flag inconsistent references"
118        );
119    }
120
121    #[test]
122    fn test_rf03_accepts_all_qualified() {
123        let violations = lint_sql(
124            "SELECT u.id, u.name FROM users u JOIN orders o ON u.id = o.user_id",
125            RuleRF03,
126        );
127        assert_eq!(violations.len(), 0);
128    }
129
130    #[test]
131    fn test_rf03_accepts_all_unqualified() {
132        let violations = lint_sql("SELECT id, name FROM users", RuleRF03);
133        assert_eq!(violations.len(), 0);
134    }
135}