Skip to main content

rigsql_rules/aliasing/
al05.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// AL05: Tables/CTEs should not be unused.
7///
8/// Detects WITH clauses where a CTE name is defined but never referenced.
9#[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        // Collect CTE names
42        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        // Get the full source text of the statement (parent or root)
56        let statement = ctx.parent.unwrap_or(ctx.root);
57        let raw = statement.raw().to_lowercase();
58
59        let mut violations = Vec::new();
60        for (name, span) in &cte_names {
61            // Simple heuristic: check if the CTE name appears elsewhere in the statement
62            // beyond its own definition. Count occurrences.
63            let count = raw.matches(name.as_str()).count();
64            // The name appears at least once in its own definition, so if count <= 1, unused
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    // CteDefinition children: name (Identifier) [WS] AS [WS] ( subquery )
80    for child in cte_def.children() {
81        let st = child.segment_type();
82        if st == SegmentType::Identifier || st == SegmentType::QuotedIdentifier {
83            if let Segment::Token(t) = child {
84                return Some(t.token.text.to_string());
85            }
86        }
87        if st == SegmentType::Keyword {
88            // Stop at AS keyword
89            break;
90        }
91    }
92    None
93}