sqrust_rules/convention/
concat_operator.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct ConcatOperator;
4
5fn line_col(source: &str, offset: usize) -> (usize, usize) {
7 let before = &source[..offset];
8 let line = before.chars().filter(|&c| c == '\n').count() + 1;
9 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) + 1;
10 (line, col)
11}
12
13fn build_skip(bytes: &[u8]) -> Vec<bool> {
16 let len = bytes.len();
17 let mut skip = vec![false; len];
18 let mut i = 0;
19
20 while i < len {
21 if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
23 skip[i] = true;
24 skip[i + 1] = true;
25 i += 2;
26 while i < len && bytes[i] != b'\n' {
27 skip[i] = true;
28 i += 1;
29 }
30 continue;
31 }
32
33 if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
35 skip[i] = true;
36 skip[i + 1] = true;
37 i += 2;
38 while i < len {
39 if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'/' {
40 skip[i] = true;
41 skip[i + 1] = true;
42 i += 2;
43 break;
44 }
45 skip[i] = true;
46 i += 1;
47 }
48 continue;
49 }
50
51 if bytes[i] == b'\'' {
53 skip[i] = true;
54 i += 1;
55 while i < len {
56 if bytes[i] == b'\'' {
57 skip[i] = true;
58 i += 1;
59 if i < len && bytes[i] == b'\'' {
60 skip[i] = true;
61 i += 1;
62 continue;
63 }
64 break;
65 }
66 skip[i] = true;
67 i += 1;
68 }
69 continue;
70 }
71
72 if bytes[i] == b'"' {
74 skip[i] = true;
75 i += 1;
76 while i < len && bytes[i] != b'"' {
77 skip[i] = true;
78 i += 1;
79 }
80 if i < len {
81 skip[i] = true;
82 i += 1;
83 }
84 continue;
85 }
86
87 if bytes[i] == b'`' {
89 skip[i] = true;
90 i += 1;
91 while i < len && bytes[i] != b'`' {
92 skip[i] = true;
93 i += 1;
94 }
95 if i < len {
96 skip[i] = true;
97 i += 1;
98 }
99 continue;
100 }
101
102 i += 1;
103 }
104
105 skip
106}
107
108fn find_concat_offsets(source: &str, skip: &[bool]) -> Vec<usize> {
111 let bytes = source.as_bytes();
112 let len = bytes.len();
113 let mut results = Vec::new();
114 let mut i = 0;
115
116 while i + 1 < len {
117 if skip[i] {
118 i += 1;
119 continue;
120 }
121
122 if bytes[i] == b'|' && bytes[i + 1] == b'|' && !skip[i + 1] {
123 results.push(i);
124 i += 2;
126 continue;
127 }
128
129 i += 1;
130 }
131
132 results
133}
134
135impl Rule for ConcatOperator {
136 fn name(&self) -> &'static str {
137 "Convention/ConcatOperator"
138 }
139
140 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
141 if !ctx.parse_errors.is_empty() {
142 return Vec::new();
143 }
144
145 let source = &ctx.source;
146 let bytes = source.as_bytes();
147 let skip = build_skip(bytes);
148 let offsets = find_concat_offsets(source, &skip);
149
150 offsets
151 .into_iter()
152 .map(|offset| {
153 let (line, col) = line_col(source, offset);
154 Diagnostic {
155 rule: self.name(),
156 message: "Use CONCAT() instead of || for cross-database portability"
157 .to_string(),
158 line,
159 col,
160 }
161 })
162 .collect()
163 }
164}