sqrust_rules/layout/
space_before_comma.rs1use sqrust_core::{Diagnostic, FileContext, Rule};
2use crate::capitalisation::SkipMap;
3
4pub struct SpaceBeforeComma;
5
6impl Rule for SpaceBeforeComma {
7 fn name(&self) -> &'static str {
8 "Layout/SpaceBeforeComma"
9 }
10
11 fn check(&self, ctx: &FileContext) -> Vec<Diagnostic> {
12 let source = ctx.source.as_bytes();
13 let len = source.len();
14 if len == 0 {
15 return Vec::new();
16 }
17
18 let skip_map = SkipMap::build(&ctx.source);
19 let mut diags = Vec::new();
20
21 for i in 0..len {
22 if source[i] != b',' {
23 continue;
24 }
25 if !skip_map.is_code(i) {
26 continue;
27 }
28
29 if i == 0 {
31 continue;
32 }
33
34 let prev = source[i - 1];
35 if prev != b' ' && prev != b'\t' {
36 continue;
38 }
39
40 let mut is_leading_comma = true;
46 let mut j = i.wrapping_sub(1);
47 loop {
48 let ch = source[j];
49 if ch == b'\n' {
50 break;
53 }
54 if ch != b' ' && ch != b'\t' {
55 is_leading_comma = false;
57 break;
58 }
59 if j == 0 {
60 break;
62 }
63 j -= 1;
64 }
65
66 if is_leading_comma {
67 continue;
68 }
69
70 let mut space_start = i - 1;
73 while space_start > 0
74 && (source[space_start - 1] == b' ' || source[space_start - 1] == b'\t')
75 {
76 space_start -= 1;
77 }
78
79 let (line, col) = byte_offset_to_line_col(&ctx.source, space_start);
80 diags.push(Diagnostic {
81 rule: self.name(),
82 message: "Remove space before comma".to_string(),
83 line,
84 col,
85 });
86 }
87
88 diags
89 }
90
91 fn fix(&self, ctx: &FileContext) -> Option<String> {
92 let source = ctx.source.as_bytes();
93 let len = source.len();
94 if len == 0 {
95 return None;
96 }
97
98 let skip_map = SkipMap::build(&ctx.source);
99 let mut result = Vec::with_capacity(len);
100 let mut changed = false;
101
102 let mut remove = vec![false; len];
105
106 for i in 0..len {
107 if source[i] != b',' {
108 continue;
109 }
110 if !skip_map.is_code(i) {
111 continue;
112 }
113 if i == 0 {
114 continue;
115 }
116 let prev = source[i - 1];
117 if prev != b' ' && prev != b'\t' {
118 continue;
119 }
120
121 let mut is_leading_comma = true;
123 let mut j = i.wrapping_sub(1);
124 loop {
125 let ch = source[j];
126 if ch == b'\n' {
127 break;
128 }
129 if ch != b' ' && ch != b'\t' {
130 is_leading_comma = false;
131 break;
132 }
133 if j == 0 {
134 break;
135 }
136 j -= 1;
137 }
138
139 if is_leading_comma {
140 continue;
141 }
142
143 let mut space_start = i - 1;
145 while space_start > 0
146 && (source[space_start - 1] == b' ' || source[space_start - 1] == b'\t')
147 {
148 space_start -= 1;
149 }
150 for k in space_start..i {
151 remove[k] = true;
152 changed = true;
153 }
154 }
155
156 if !changed {
157 return None;
158 }
159
160 for (idx, &byte) in source.iter().enumerate() {
161 if !remove[idx] {
162 result.push(byte);
163 }
164 }
165
166 Some(String::from_utf8(result).expect("source was valid UTF-8"))
167 }
168}
169
170fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
172 let mut line = 1usize;
173 let mut line_start = 0usize;
174 for (i, ch) in source.char_indices() {
175 if i == offset {
176 break;
177 }
178 if ch == '\n' {
179 line += 1;
180 line_start = i + 1;
181 }
182 }
183 let col = offset - line_start + 1;
184 (line, col)
185}