Skip to main content

rigsql_rules/convention/
cv10.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::{LintViolation, SourceEdit};
5
6/// CV10: Consistent usage of preferred quotes for quoted literals.
7///
8/// By default, prefer single quotes for string literals.
9#[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_and_msg_key(
83                    self.code(),
84                    "Use single quotes for string literals.",
85                    t.token.span,
86                    vec![SourceEdit::replace(t.token.span, new_text)],
87                    "rules.CV10.msg.single",
88                    vec![],
89                )]
90            }
91            QuoteStyle::Double if uses_single => {
92                let inner = &text[1..text.len() - 1];
93                let replaced = inner.replace('"', "\"\"").replace("''", "'");
94                let new_text = format!("\"{}\"", replaced);
95                vec![LintViolation::with_fix_and_msg_key(
96                    self.code(),
97                    "Use double quotes for string literals.",
98                    t.token.span,
99                    vec![SourceEdit::replace(t.token.span, new_text)],
100                    "rules.CV10.msg.double",
101                    vec![],
102                )]
103            }
104            _ => vec![],
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::test_utils::lint_sql;
113
114    #[test]
115    fn test_cv10_accepts_single_quotes() {
116        let violations = lint_sql("SELECT 'hello' FROM t", RuleCV10::default());
117        assert_eq!(violations.len(), 0);
118    }
119
120    #[test]
121    fn test_cv10_accepts_non_string() {
122        let violations = lint_sql("SELECT 1 FROM t", RuleCV10::default());
123        assert_eq!(violations.len(), 0);
124    }
125}