Skip to main content

rigsql_rules/references/
rf06.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6/// RF06: Unnecessary quoting of identifiers.
7///
8/// If a quoted identifier (e.g., `"my_col"`) contains only alphanumeric
9/// characters and underscores, and starts with a letter or underscore,
10/// it could be written as a bare identifier. The quotes are unnecessary.
11#[derive(Debug, Default)]
12pub struct RuleRF06;
13
14impl Rule for RuleRF06 {
15    fn code(&self) -> &'static str {
16        "RF06"
17    }
18    fn name(&self) -> &'static str {
19        "references.quoting"
20    }
21    fn description(&self) -> &'static str {
22        "Unnecessary quoting of identifiers."
23    }
24    fn explanation(&self) -> &'static str {
25        "Quoted identifiers that contain only alphanumeric characters, underscores, \
26         and start with a letter or underscore do not need to be quoted. Removing \
27         unnecessary quotes improves readability. Quoting should be reserved for \
28         identifiers that genuinely require it (e.g., reserved words, spaces, special characters)."
29    }
30    fn groups(&self) -> &[RuleGroup] {
31        &[RuleGroup::References]
32    }
33    fn is_fixable(&self) -> bool {
34        true
35    }
36
37    fn crawl_type(&self) -> CrawlType {
38        CrawlType::Segment(vec![SegmentType::QuotedIdentifier])
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
48        // Strip surrounding quotes (double quotes, backticks, or brackets)
49        let inner = strip_quotes(text);
50        let Some(inner) = inner else {
51            return vec![];
52        };
53
54        if inner.is_empty() {
55            return vec![];
56        }
57
58        // Check if the inner text could be a bare identifier:
59        // starts with letter or underscore, and only contains alphanumeric + underscore
60        let first = inner.chars().next().unwrap();
61        if !(first.is_ascii_alphabetic() || first == '_') {
62            return vec![];
63        }
64
65        let is_simple = inner.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
66
67        if is_simple {
68            vec![LintViolation::with_fix(
69                self.code(),
70                format!("Identifier '{}' does not need quoting.", text),
71                t.token.span,
72                vec![SourceEdit::replace(t.token.span, inner.to_string())],
73            )]
74        } else {
75            vec![]
76        }
77    }
78}
79
80fn strip_quotes(text: &str) -> Option<&str> {
81    if text.len() < 2 {
82        return None;
83    }
84    let bytes = text.as_bytes();
85    match (bytes[0], bytes[bytes.len() - 1]) {
86        (b'"', b'"') | (b'`', b'`') => Some(&text[1..text.len() - 1]),
87        (b'[', b']') => Some(&text[1..text.len() - 1]),
88        _ => None,
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::test_utils::lint_sql;
96
97    #[test]
98    fn test_rf06_flags_unnecessary_quoting() {
99        let violations = lint_sql("SELECT \"my_col\" FROM t", RuleRF06);
100        assert!(
101            !violations.is_empty(),
102            "Should flag unnecessarily quoted identifier"
103        );
104        assert!(violations[0].message.contains("my_col"));
105        assert!(!violations[0].fixes.is_empty(), "Should provide a fix");
106    }
107
108    #[test]
109    fn test_rf06_accepts_necessary_quoting() {
110        let violations = lint_sql("SELECT \"my-col\" FROM t", RuleRF06);
111        assert_eq!(violations.len(), 0);
112    }
113
114    #[test]
115    fn test_rf06_accepts_bare_identifiers() {
116        let violations = lint_sql("SELECT my_col FROM t", RuleRF06);
117        assert_eq!(violations.len(), 0);
118    }
119}