rigsql_rules/aliasing/
al05.rs1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6#[derive(Debug, Default)]
10pub struct RuleAL05;
11
12impl Rule for RuleAL05 {
13 fn code(&self) -> &'static str {
14 "AL05"
15 }
16 fn name(&self) -> &'static str {
17 "aliasing.unused"
18 }
19 fn description(&self) -> &'static str {
20 "Tables/CTEs should not be unused."
21 }
22 fn explanation(&self) -> &'static str {
23 "Every CTE (Common Table Expression) defined in a WITH clause should be \
24 referenced in the main query or in another CTE. Unused CTEs add complexity \
25 without benefit and should be removed."
26 }
27 fn groups(&self) -> &[RuleGroup] {
28 &[RuleGroup::Aliasing]
29 }
30 fn is_fixable(&self) -> bool {
31 false
32 }
33
34 fn crawl_type(&self) -> CrawlType {
35 CrawlType::Segment(vec![SegmentType::WithClause])
36 }
37
38 fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
39 let children = ctx.segment.children();
40
41 let mut cte_names: Vec<(String, rigsql_core::Span)> = Vec::new();
43 for child in children {
44 if child.segment_type() == SegmentType::CteDefinition {
45 if let Some(name) = extract_cte_name(child) {
46 cte_names.push((name.to_lowercase(), child.span()));
47 }
48 }
49 }
50
51 if cte_names.is_empty() {
52 return vec![];
53 }
54
55 let raw = ctx.root.raw().to_lowercase();
59
60 let mut violations = Vec::new();
61 for (name, span) in &cte_names {
62 let count = raw.matches(name.as_str()).count();
65 if count <= 1 {
67 violations.push(LintViolation::new(
68 self.code(),
69 format!("CTE '{}' is defined but not used.", name),
70 *span,
71 ));
72 }
73 }
74
75 violations
76 }
77}
78
79fn extract_cte_name(cte_def: &Segment) -> Option<String> {
80 for child in cte_def.children() {
82 let st = child.segment_type();
83 if st == SegmentType::Identifier || st == SegmentType::QuotedIdentifier {
84 if let Segment::Token(t) = child {
85 return Some(t.token.text.to_string());
86 }
87 }
88 if st == SegmentType::Keyword {
89 break;
91 }
92 }
93 None
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::test_utils::lint_sql;
100
101 #[test]
102 fn test_al05_flags_unused_cte() {
103 let violations = lint_sql(
104 "WITH unused AS (SELECT 1) SELECT * FROM other_table",
105 RuleAL05,
106 );
107 assert_eq!(violations.len(), 1);
108 }
109
110 #[test]
111 fn test_al05_accepts_used_cte() {
112 let violations = lint_sql("WITH cte AS (SELECT 1) SELECT * FROM cte", RuleAL05);
113 assert_eq!(violations.len(), 0);
114 }
115}