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::with_msg_key(
68 self.code(),
69 format!("CTE '{}' is defined but not used.", name),
70 *span,
71 "rules.AL05.msg",
72 vec![("name".to_string(), name.to_string())],
73 ));
74 }
75 }
76
77 violations
78 }
79}
80
81fn extract_cte_name(cte_def: &Segment) -> Option<String> {
82 for child in cte_def.children() {
84 let st = child.segment_type();
85 if st == SegmentType::Identifier || st == SegmentType::QuotedIdentifier {
86 if let Segment::Token(t) = child {
87 return Some(t.token.text.to_string());
88 }
89 }
90 if st == SegmentType::Keyword {
91 break;
93 }
94 }
95 None
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::test_utils::lint_sql;
102
103 #[test]
104 fn test_al05_flags_unused_cte() {
105 let violations = lint_sql(
106 "WITH unused AS (SELECT 1) SELECT * FROM other_table",
107 RuleAL05,
108 );
109 assert_eq!(violations.len(), 1);
110 }
111
112 #[test]
113 fn test_al05_accepts_used_cte() {
114 let violations = lint_sql("WITH cte AS (SELECT 1) SELECT * FROM cte", RuleAL05);
115 assert_eq!(violations.len(), 0);
116 }
117}