rigsql_rules/convention/
cv10.rs1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6#[derive(Debug)]
10pub struct RuleCV10 {
11 pub preferred_style: QuoteStyle,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum QuoteStyle {
16 Single,
17 Double,
18}
19
20impl Default for RuleCV10 {
21 fn default() -> Self {
22 Self {
23 preferred_style: QuoteStyle::Single,
24 }
25 }
26}
27
28impl Rule for RuleCV10 {
29 fn code(&self) -> &'static str {
30 "CV10"
31 }
32 fn name(&self) -> &'static str {
33 "convention.quoted_literals"
34 }
35 fn description(&self) -> &'static str {
36 "Consistent usage of preferred quotes for quoted literals."
37 }
38 fn explanation(&self) -> &'static str {
39 "String literals should use a consistent quoting style. By default, \
40 single quotes are preferred as they are the ANSI SQL standard for \
41 string literals."
42 }
43 fn groups(&self) -> &[RuleGroup] {
44 &[RuleGroup::Convention]
45 }
46 fn is_fixable(&self) -> bool {
47 true
48 }
49
50 fn configure(&mut self, settings: &std::collections::HashMap<String, String>) {
51 if let Some(val) = settings.get("preferred_quoted_literal_style") {
52 self.preferred_style = match val.as_str() {
53 "double" => QuoteStyle::Double,
54 _ => QuoteStyle::Single,
55 };
56 }
57 }
58
59 fn crawl_type(&self) -> CrawlType {
60 CrawlType::Segment(vec![SegmentType::StringLiteral])
61 }
62
63 fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
64 let Segment::Token(t) = ctx.segment else {
65 return vec![];
66 };
67
68 let text = t.token.text.as_str();
69 if text.len() < 2 {
70 return vec![];
71 }
72
73 let first_char = text.as_bytes()[0];
74 let uses_single = first_char == b'\'';
75 let uses_double = first_char == b'"';
76
77 match self.preferred_style {
78 QuoteStyle::Single if uses_double => {
79 let inner = &text[1..text.len() - 1];
80 let replaced = inner.replace('\'', "''").replace("\"\"", "\"");
81 let new_text = format!("'{}'", replaced);
82 vec![LintViolation::with_fix(
83 self.code(),
84 "Use single quotes for string literals.",
85 t.token.span,
86 vec![SourceEdit::replace(t.token.span, new_text)],
87 )]
88 }
89 QuoteStyle::Double if uses_single => {
90 let inner = &text[1..text.len() - 1];
91 let replaced = inner.replace('"', "\"\"").replace("''", "'");
92 let new_text = format!("\"{}\"", replaced);
93 vec![LintViolation::with_fix(
94 self.code(),
95 "Use double quotes for string literals.",
96 t.token.span,
97 vec![SourceEdit::replace(t.token.span, new_text)],
98 )]
99 }
100 _ => vec![],
101 }
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::test_utils::lint_sql;
109
110 #[test]
111 fn test_cv10_accepts_single_quotes() {
112 let violations = lint_sql("SELECT 'hello' FROM t", RuleCV10::default());
113 assert_eq!(violations.len(), 0);
114 }
115
116 #[test]
117 fn test_cv10_accepts_non_string() {
118 let violations = lint_sql("SELECT 1 FROM t", RuleCV10::default());
119 assert_eq!(violations.len(), 0);
120 }
121}