rigsql_rules/references/
rf06.rs1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6#[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 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 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_and_msg_key(
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 "rules.RF06.msg",
74 vec![("name".to_string(), text.to_string())],
75 )]
76 } else {
77 vec![]
78 }
79 }
80}
81
82fn strip_quotes(text: &str) -> Option<&str> {
83 if text.len() < 2 {
84 return None;
85 }
86 let bytes = text.as_bytes();
87 match (bytes[0], bytes[bytes.len() - 1]) {
88 (b'"', b'"') | (b'`', b'`') => Some(&text[1..text.len() - 1]),
89 (b'[', b']') => Some(&text[1..text.len() - 1]),
90 _ => None,
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::test_utils::lint_sql;
98
99 #[test]
100 fn test_rf06_flags_unnecessary_quoting() {
101 let violations = lint_sql("SELECT \"my_col\" FROM t", RuleRF06);
102 assert!(
103 !violations.is_empty(),
104 "Should flag unnecessarily quoted identifier"
105 );
106 assert!(violations[0].message.contains("my_col"));
107 assert!(!violations[0].fixes.is_empty(), "Should provide a fix");
108 }
109
110 #[test]
111 fn test_rf06_accepts_necessary_quoting() {
112 let violations = lint_sql("SELECT \"my-col\" FROM t", RuleRF06);
113 assert_eq!(violations.len(), 0);
114 }
115
116 #[test]
117 fn test_rf06_accepts_bare_identifiers() {
118 let violations = lint_sql("SELECT my_col FROM t", RuleRF06);
119 assert_eq!(violations.len(), 0);
120 }
121}