Skip to main content

rigsql_rules/references/
rf04.rs

1use rigsql_core::{Segment, SegmentType};
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::violation::LintViolation;
5
6/// RF04: Keywords used as identifiers.
7///
8/// Detects identifiers that match SQL reserved keywords, which can cause
9/// confusion and portability issues.
10#[derive(Debug, Default)]
11pub struct RuleRF04;
12
13const RESERVED_KEYWORDS: &[&str] = &[
14    "SELECT",
15    "FROM",
16    "WHERE",
17    "INSERT",
18    "UPDATE",
19    "DELETE",
20    "CREATE",
21    "DROP",
22    "ALTER",
23    "TABLE",
24    "INDEX",
25    "VIEW",
26    "JOIN",
27    "ON",
28    "AND",
29    "OR",
30    "NOT",
31    "IN",
32    "IS",
33    "NULL",
34    "TRUE",
35    "FALSE",
36    "BETWEEN",
37    "LIKE",
38    "ORDER",
39    "BY",
40    "GROUP",
41    "HAVING",
42    "LIMIT",
43    "OFFSET",
44    "UNION",
45    "EXCEPT",
46    "INTERSECT",
47    "INTO",
48    "VALUES",
49    "SET",
50    "AS",
51    "CASE",
52    "WHEN",
53    "THEN",
54    "ELSE",
55    "END",
56    "EXISTS",
57    "ALL",
58    "ANY",
59    "DISTINCT",
60    "TOP",
61    "COUNT",
62    "SUM",
63    "AVG",
64    "MIN",
65    "MAX",
66    "CAST",
67    "COALESCE",
68    "NULLIF",
69];
70
71impl Rule for RuleRF04 {
72    fn code(&self) -> &'static str {
73        "RF04"
74    }
75    fn name(&self) -> &'static str {
76        "references.keywords"
77    }
78    fn description(&self) -> &'static str {
79        "Keywords should not be used as identifiers."
80    }
81    fn explanation(&self) -> &'static str {
82        "Using SQL reserved keywords as identifiers (column names, table names, aliases) \
83         can cause parsing ambiguity, confuse readers, and reduce portability across \
84         SQL dialects. Use descriptive, non-reserved names instead."
85    }
86    fn groups(&self) -> &[RuleGroup] {
87        &[RuleGroup::References]
88    }
89    fn is_fixable(&self) -> bool {
90        false
91    }
92
93    fn crawl_type(&self) -> CrawlType {
94        CrawlType::Segment(vec![SegmentType::Identifier])
95    }
96
97    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
98        let Segment::Token(t) = ctx.segment else {
99            return vec![];
100        };
101
102        let upper = t.token.text.to_ascii_uppercase();
103        if RESERVED_KEYWORDS.contains(&upper.as_str()) {
104            vec![LintViolation::new(
105                self.code(),
106                format!("Identifier '{}' is a reserved keyword.", t.token.text),
107                t.token.span,
108            )]
109        } else {
110            vec![]
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::test_utils::lint_sql;
119
120    #[test]
121    fn test_rf04_flags_keyword_as_identifier() {
122        // "table" used as a table name identifier
123        let violations = lint_sql("SELECT id FROM \"table\"", RuleRF04);
124        // QuotedIdentifier won't be visited since we crawl Identifier only
125        assert_eq!(violations.len(), 0);
126    }
127
128    #[test]
129    fn test_rf04_accepts_normal_identifiers() {
130        let violations = lint_sql("SELECT user_id, email FROM users", RuleRF04);
131        assert_eq!(violations.len(), 0);
132    }
133
134    #[test]
135    fn test_rf04_flags_keyword_identifier_in_alias() {
136        // If the parser emits "table" as an Identifier segment, this should flag it
137        let violations = lint_sql("SELECT 1 AS \"values\"", RuleRF04);
138        // "values" in quotes is QuotedIdentifier, not Identifier — so no violation
139        assert_eq!(violations.len(), 0);
140    }
141}