rigsql_rules/references/
rf03.rs1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6#[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 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 SegmentType::Identifier => {
50 if let Segment::Token(t) = sel_child {
51 unqualified.push(t.token.span);
52 }
53 }
54 SegmentType::ColumnRef => {
56 qualified_count += 1;
57 }
58 SegmentType::AliasExpression => {
60 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; }
78 }
79 _ => {}
80 }
81 }
82 }
83 }
84
85 if qualified_count == 0 || unqualified.is_empty() {
87 return vec![];
88 }
89
90 unqualified
91 .iter()
92 .map(|span| {
93 LintViolation::with_msg_key(
94 self.code(),
95 "Inconsistent column qualification. Mix of qualified and unqualified references."
96 .to_string(),
97 *span,
98 "rules.RF03.msg",
99 vec![],
100 )
101 })
102 .collect()
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::test_utils::lint_sql;
110
111 #[test]
112 fn test_rf03_flags_inconsistent_qualification() {
113 let violations = lint_sql(
114 "SELECT u.id, name FROM users u JOIN orders o ON u.id = o.user_id",
115 RuleRF03,
116 );
117 assert!(
118 !violations.is_empty(),
119 "Should flag inconsistent references"
120 );
121 }
122
123 #[test]
124 fn test_rf03_accepts_all_qualified() {
125 let violations = lint_sql(
126 "SELECT u.id, u.name FROM users u JOIN orders o ON u.id = o.user_id",
127 RuleRF03,
128 );
129 assert_eq!(violations.len(), 0);
130 }
131
132 #[test]
133 fn test_rf03_accepts_all_unqualified() {
134 let violations = lint_sql("SELECT id, name FROM users", RuleRF03);
135 assert_eq!(violations.len(), 0);
136 }
137}