Skip to main content

rigsql_rules/aliasing/
al07.rs

1use rigsql_core::{Segment, SegmentType, Span};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::utils::is_in_table_context;
5use crate::violation::{LintViolation, SourceEdit};
6
7/// AL07: Avoid table aliases in FROM clauses and JOIN conditions.
8///
9/// Disabled by default. When enabled, flags all table aliases and suggests
10/// using the full table name instead.
11#[derive(Debug, Default)]
12pub struct RuleAL07 {
13    pub force_enable: bool,
14}
15
16impl Rule for RuleAL07 {
17    fn code(&self) -> &'static str {
18        "AL07"
19    }
20    fn name(&self) -> &'static str {
21        "aliasing.forbid"
22    }
23    fn description(&self) -> &'static str {
24        "Avoid table aliases in FROM clauses and JOIN conditions."
25    }
26    fn explanation(&self) -> &'static str {
27        "Table aliases can reduce readability, especially initialisms. Using the \
28         full table name makes it clear where each column comes from. This rule \
29         is disabled by default as it is controversial for larger databases."
30    }
31    fn groups(&self) -> &[RuleGroup] {
32        &[RuleGroup::Aliasing]
33    }
34    fn is_fixable(&self) -> bool {
35        true
36    }
37
38    fn configure(&mut self, settings: &std::collections::HashMap<String, String>) {
39        if let Some(val) = settings.get("force_enable") {
40            self.force_enable = val.eq_ignore_ascii_case("true") || val == "1";
41        }
42    }
43
44    fn crawl_type(&self) -> CrawlType {
45        CrawlType::Segment(vec![SegmentType::AliasExpression])
46    }
47
48    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
49        if !self.force_enable {
50            return vec![];
51        }
52
53        if !is_in_table_context(ctx) {
54            return vec![];
55        }
56
57        // Find the span of the alias part (AS keyword + alias name) to delete
58        let children = ctx.segment.children();
59        let mut alias_start: Option<Span> = None;
60        let mut alias_end: Option<Span> = None;
61        let mut found_table = false;
62
63        for child in children {
64            let st = child.segment_type();
65            if !found_table {
66                if st == SegmentType::Identifier
67                    || st == SegmentType::QuotedIdentifier
68                    || st == SegmentType::Keyword
69                {
70                    found_table = true;
71                }
72                continue;
73            }
74
75            // Everything after the table name is the alias part
76            if alias_start.is_none() {
77                // Include preceding whitespace
78                if let Segment::Token(_) = child {
79                    alias_start = Some(child.span());
80                }
81            }
82            alias_end = Some(child.span());
83        }
84
85        if let (Some(start), Some(end)) = (alias_start, alias_end) {
86            let delete_span = start.merge(end);
87            vec![LintViolation::with_fix(
88                self.code(),
89                "Avoid using table aliases. Use the full table name instead.",
90                ctx.segment.span(),
91                vec![SourceEdit::delete(delete_span)],
92            )]
93        } else {
94            vec![LintViolation::new(
95                self.code(),
96                "Avoid using table aliases. Use the full table name instead.",
97                ctx.segment.span(),
98            )]
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::test_utils::lint_sql;
107
108    #[test]
109    fn test_al07_disabled_by_default() {
110        let violations = lint_sql("SELECT * FROM users AS u", RuleAL07::default());
111        assert_eq!(violations.len(), 0);
112    }
113
114    #[test]
115    fn test_al07_enabled_flags_table_alias() {
116        let rule = RuleAL07 { force_enable: true };
117        let violations = lint_sql("SELECT * FROM users AS u", rule);
118        assert!(!violations.is_empty());
119    }
120
121    #[test]
122    fn test_al07_skips_column_alias() {
123        let rule = RuleAL07 { force_enable: true };
124        let violations = lint_sql("SELECT col AS c FROM t", rule);
125        // col AS c is in SelectClause, not FROM/JOIN, so no violation
126        assert_eq!(violations.len(), 0);
127    }
128}