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::with_msg_key(
67 self.code(),
68 format!("CTE '{}' is defined but not used.", name),
69 *span,
70 "rules.ST03.msg",
71 vec![("name".to_string(), name.to_string())],
72 ));
73 }
74 }
75
76 violations
77 }
78}
79
80fn extract_cte_name(cte_def: &Segment) -> Option<String> {
81 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;
90 }
91 }
92 None
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::test_utils::lint_sql;
99
100 #[test]
101 fn test_st03_flags_unused_cte() {
102 let violations = lint_sql(
103 "WITH unused AS (SELECT 1) SELECT * FROM other_table;",
104 RuleST03,
105 );
106 assert_eq!(violations.len(), 1);
107 assert!(violations[0].message.contains("unused"));
108 }
109
110 #[test]
111 fn test_st03_accepts_used_cte() {
112 let violations = lint_sql("WITH cte AS (SELECT 1) SELECT * FROM cte;", RuleST03);
113 assert_eq!(violations.len(), 0);
114 }
115
116 #[test]
117 fn test_st03_accepts_no_cte() {
118 let violations = lint_sql("SELECT * FROM t;", RuleST03);
119 assert_eq!(violations.len(), 0);
120 }
121}