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