Skip to main content

rigsql_rules/references/
rf05.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// RF05: Identifiers should not contain special characters.
7///
8/// Bare identifiers should only contain alphanumeric characters and underscores.
9/// QuotedIdentifiers are excluded from this check as they may legitimately need
10/// special characters.
11#[derive(Debug, Default)]
12pub struct RuleRF05;
13
14impl Rule for RuleRF05 {
15    fn code(&self) -> &'static str {
16        "RF05"
17    }
18    fn name(&self) -> &'static str {
19        "references.special_chars"
20    }
21    fn description(&self) -> &'static str {
22        "Identifiers should not contain special characters."
23    }
24    fn explanation(&self) -> &'static str {
25        "Bare identifiers should only contain alphanumeric characters and underscores. \
26         Special characters (spaces, hyphens, etc.) in identifiers require quoting and \
27         can cause portability issues. If special characters are needed, use quoted \
28         identifiers explicitly."
29    }
30    fn groups(&self) -> &[RuleGroup] {
31        &[RuleGroup::References]
32    }
33    fn is_fixable(&self) -> bool {
34        false
35    }
36
37    fn crawl_type(&self) -> CrawlType {
38        CrawlType::Segment(vec![SegmentType::Identifier])
39    }
40
41    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
42        let Segment::Token(t) = ctx.segment else {
43            return vec![];
44        };
45
46        let text = &t.token.text;
47        let has_special = text.chars().any(|c| !c.is_alphanumeric() && c != '_');
48
49        if has_special {
50            vec![LintViolation::with_msg_key(
51                self.code(),
52                format!("Identifier '{}' contains special characters.", text),
53                t.token.span,
54                "rules.RF05.msg",
55                vec![("name".to_string(), text.to_string())],
56            )]
57        } else {
58            vec![]
59        }
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::test_utils::lint_sql;
67
68    #[test]
69    fn test_rf05_accepts_normal_identifiers() {
70        let violations = lint_sql("SELECT user_id, email FROM users_2024", RuleRF05);
71        assert_eq!(violations.len(), 0);
72    }
73
74    #[test]
75    fn test_rf05_accepts_quoted_identifiers() {
76        // QuotedIdentifier is not crawled by RF05
77        let violations = lint_sql("SELECT \"my-col\" FROM t", RuleRF05);
78        assert_eq!(violations.len(), 0);
79    }
80}