sqrust_rules/layout/
comment_spacing.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2
3pub struct CommentSpacing;
4
5impl Rule for CommentSpacing {
6 fn name(&self) -> &'static str {
7 "Layout/CommentSpacing"
8 }
9
10 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
11 find_violations(&ctx.source, self.name())
12 }
13
14 fn fix(&self, ctx: &FileContext) -> Option<String> {
15 let violations = find_violations(&ctx.source, self.name());
16 if violations.is_empty() {
17 return None;
18 }
19
20 let bytes = ctx.source.as_bytes();
24 let len = bytes.len();
25
26 let mut inserts: Vec<usize> = Vec::new();
28 let mut i = 0;
30 let mut in_string = false;
31 let mut block_depth: usize = 0;
32
33 while i < len {
34 if !in_string && block_depth == 0 && bytes[i] == b'\'' {
36 in_string = true;
37 i += 1;
38 continue;
39 }
40 if in_string {
41 if bytes[i] == b'\'' {
42 if i + 1 < len && bytes[i + 1] == b'\'' {
44 i += 2;
45 continue;
46 }
47 in_string = false;
48 }
49 i += 1;
50 continue;
51 }
52
53 if block_depth == 0 && i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
55 block_depth += 1;
56 i += 2;
57 continue;
58 }
59 if block_depth > 0 {
61 if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'/' {
62 block_depth -= 1;
63 i += 2;
64 } else {
65 i += 1;
66 }
67 continue;
68 }
69
70 if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
72 let after = i + 2;
73 let next_byte = if after < len { Some(bytes[after]) } else { None };
75 match next_byte {
76 Some(b'-') => {}
78 None | Some(b' ') | Some(b'\n') | Some(b'\r') | Some(b'\t') => {}
80 Some(_) => {
82 inserts.push(after);
83 }
84 }
85 i += 2;
87 while i < len && bytes[i] != b'\n' {
88 i += 1;
89 }
90 continue;
91 }
92
93 i += 1;
94 }
95
96 if inserts.is_empty() {
97 return None;
98 }
99
100 let mut result = Vec::with_capacity(len + inserts.len());
103 let mut prev = 0;
104 for &pos in &inserts {
105 result.extend_from_slice(&bytes[prev..pos]);
106 result.push(b' ');
107 prev = pos;
108 }
109 result.extend_from_slice(&bytes[prev..]);
110
111 Some(String::from_utf8(result).expect("source was valid UTF-8"))
112 }
113}
114
115fn find_violations(source: &str, rule_name: &'static str) -> Vec<Diagnostic> {
119 let bytes = source.as_bytes();
120 let len = bytes.len();
121 let mut diags = Vec::new();
122
123 let mut i = 0;
124 let mut in_string = false;
125 let mut block_depth: usize = 0;
126
127 while i < len {
128 if !in_string && block_depth == 0 && bytes[i] == b'\'' {
130 in_string = true;
131 i += 1;
132 continue;
133 }
134 if in_string {
135 if bytes[i] == b'\'' {
136 if i + 1 < len && bytes[i + 1] == b'\'' {
138 i += 2;
139 continue;
140 }
141 in_string = false;
142 }
143 i += 1;
144 continue;
145 }
146
147 if block_depth == 0 && i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
149 block_depth += 1;
150 i += 2;
151 continue;
152 }
153 if block_depth > 0 {
155 if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'/' {
156 block_depth -= 1;
157 i += 2;
158 } else {
159 i += 1;
160 }
161 continue;
162 }
163
164 if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
166 let after = i + 2;
167 let next_byte = if after < len { Some(bytes[after]) } else { None };
168 match next_byte {
169 Some(b'-') => {}
171 None | Some(b' ') | Some(b'\t') | Some(b'\n') | Some(b'\r') => {}
173 Some(_) => {
175 let (line, col) = byte_offset_to_line_col(source, i);
176 diags.push(Diagnostic {
177 rule: rule_name,
178 message:
179 "Line comment should have a space after '--'; write '-- comment'"
180 .to_string(),
181 line,
182 col,
183 });
184 }
185 }
186 i += 2;
188 while i < len && bytes[i] != b'\n' {
189 i += 1;
190 }
191 continue;
192 }
193
194 i += 1;
195 }
196
197 diags
198}
199
200fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
202 let mut line = 1usize;
203 let mut line_start = 0usize;
204 for (i, ch) in source.char_indices() {
205 if i == offset {
206 break;
207 }
208 if ch == '\n' {
209 line += 1;
210 line_start = i + 1;
211 }
212 }
213 let col = offset - line_start + 1;
214 (line, col)
215}