1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct MaxIdentifierLength {
4 pub max_length: usize,
5}
6
7impl Default for MaxIdentifierLength {
8 fn default() -> Self {
9 MaxIdentifierLength { max_length: 30 }
10 }
11}
12
13impl Rule for MaxIdentifierLength {
14 fn name(&self) -> &'static str {
15 "Layout/MaxIdentifierLength"
16 }
17
18 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
19 if !ctx.parse_errors.is_empty() {
20 return Vec::new();
21 }
22
23 let max = self.max_length;
24 let mut diags = Vec::new();
25
26 let src = &ctx.source;
29 let bytes = src.as_bytes();
30 let len = bytes.len();
31 let mut i = 0;
32
33 while i < len {
34 let ch = bytes[i];
35
36 if ch == b'\'' {
38 i += 1;
39 while i < len && bytes[i] != b'\'' {
40 if bytes[i] == b'\\' {
41 i += 1;
42 }
43 i += 1;
44 }
45 i += 1; continue;
47 }
48
49 if i + 1 < len && ch == b'-' && bytes[i + 1] == b'-' {
51 while i < len && bytes[i] != b'\n' {
52 i += 1;
53 }
54 continue;
55 }
56
57 if ch == b'"' || ch == b'`' {
59 let close = ch;
60 let start = i;
61 i += 1;
62 let content_start = i;
63 while i < len && bytes[i] != close {
64 i += 1;
65 }
66 let content = &src[content_start..i];
67 i += 1; if content.len() > max {
69 let (line, col) = offset_to_line_col(src, start);
70 diags.push(Diagnostic {
71 rule: "Layout/MaxIdentifierLength",
72 message: format!(
73 "Identifier '{}' is {} characters long; maximum is {}",
74 content,
75 content.len(),
76 max
77 ),
78 line,
79 col,
80 });
81 }
82 continue;
83 }
84
85 if ch.is_ascii_alphabetic() || ch == b'_' {
87 let start = i;
88 while i < len && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
89 i += 1;
90 }
91 let ident = &src[start..i];
92 if !is_keyword(ident) && ident.len() > max {
94 let (line, col) = offset_to_line_col(src, start);
95 diags.push(Diagnostic {
96 rule: "Layout/MaxIdentifierLength",
97 message: format!(
98 "Identifier '{}' is {} characters long; maximum is {}",
99 ident,
100 ident.len(),
101 max
102 ),
103 line,
104 col,
105 });
106 }
107 continue;
108 }
109
110 i += 1;
111 }
112
113 diags
114 }
115}
116
117fn is_keyword(s: &str) -> bool {
118 const KEYWORDS: &[&str] = &[
119 "SELECT", "FROM", "WHERE", "JOIN", "INNER", "LEFT", "RIGHT", "FULL",
120 "OUTER", "ON", "AND", "OR", "NOT", "IN", "IS", "NULL", "AS", "BY",
121 "GROUP", "ORDER", "HAVING", "LIMIT", "OFFSET", "UNION", "ALL",
122 "DISTINCT", "INSERT", "INTO", "UPDATE", "SET", "DELETE", "CREATE",
123 "TABLE", "ALTER", "DROP", "WITH", "CASE", "WHEN", "THEN", "ELSE",
124 "END", "EXISTS", "BETWEEN", "LIKE", "ASC", "DESC", "PRIMARY", "KEY",
125 "FOREIGN", "REFERENCES", "INDEX", "VIEW", "CROSS", "NATURAL",
126 "USING", "VALUES", "DEFAULT", "CONSTRAINT", "UNIQUE", "CHECK",
127 "RETURNS", "RETURN", "BEGIN", "COMMIT", "ROLLBACK", "TRANSACTION",
128 "TRUE", "FALSE", "EXCEPT", "INTERSECT", "RECURSIVE", "LATERAL",
129 "REPLACE", "IF", "COUNT", "SUM", "AVG", "MIN", "MAX", "ABS", "YEAR",
130 "MONTH", "DAY", "UPPER", "LOWER", "LENGTH", "TRIM", "COALESCE",
131 "NULLIF", "CAST", "CONVERT", "SUBSTRING", "CONCAT", "REPLACE",
132 "OVER", "PARTITION", "ROWS", "RANGE", "UNBOUNDED", "PRECEDING",
133 "FOLLOWING", "CURRENT", "ROW",
134 ];
135 KEYWORDS
136 .iter()
137 .any(|k| k.eq_ignore_ascii_case(s))
138}
139
140fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
141 let before = &source[..offset];
142 let line = before.chars().filter(|&c| c == '\n').count() + 1;
143 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
144 (line, col)
145}