1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
use std::{fmt, sync::Arc};
use serde::{Serialize, Serializer};
/// Source location information for AST nodes
#[derive(Clone, Default)]
pub struct SourceLocation {
pub line: usize,
pub column: usize,
pub start_byte: usize,
pub end_byte: usize,
source: Arc<Vec<u8>>,
}
impl fmt::Debug for SourceLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
impl Serialize for SourceLocation {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&format!("{}:{}", self.line, self.column))
}
}
impl SourceLocation {
pub fn new(
line: usize,
column: usize,
start_byte: usize,
end_byte: usize,
source: Arc<Vec<u8>>,
) -> Self {
Self {
line,
column,
start_byte,
end_byte,
source,
}
}
/// Create a new `SourceLocation` sharing the same source but pointing at
/// a different byte range. Line/column are zeroed since they are only used
/// for display and these synthetic locations are used for fix generation.
pub fn with_byte_range(&self, start_byte: usize, end_byte: usize) -> Self {
Self {
line: 0,
column: 0,
start_byte,
end_byte,
source: Arc::clone(&self.source),
}
}
/// Get the shared source bytes
pub fn source(&self) -> &[u8] {
&self.source
}
/// Extract the source text for this location
pub fn as_str(&self) -> Option<&str> {
std::str::from_utf8(&self.source[self.start_byte..self.end_byte]).ok()
}
/// Find the byte position of the start of the line containing this location
fn find_line_start(&self) -> usize {
let mut line_start = self.start_byte;
while line_start > 0 && self.source[line_start - 1] != b'\n' {
line_start -= 1;
}
line_start
}
/// Find the byte position just past the newline at the end of the
/// statement/expression. Scans forward from `end_byte`.
fn find_line_end(&self) -> usize {
let mut pos = self.end_byte;
while pos < self.source.len() && self.source[pos] != b'\n' {
pos += 1;
}
if pos < self.source.len() {
pos += 1;
}
pos
}
/// Return (start, end) covering the full line from its first byte
/// to just past the trailing newline — no blank-line absorption.
pub fn find_line_range(&self) -> (usize, usize) {
(self.find_line_start(), self.find_line_end())
}
/// Find the start and end byte positions of the line containing this
/// location Returns (line_start_byte, line_end_byte) including the
/// newline If the previous line is empty (only whitespace), includes it
/// too
pub fn find_line_bounds(&self) -> (usize, usize) {
// Find the start of the line
let mut line_start = self.find_line_start();
// Check if the previous line is empty (only whitespace)
if line_start > 0 {
let mut prev_line_start = line_start - 1; // Skip the '\n'
while prev_line_start > 0 && self.source[prev_line_start - 1] != b'\n' {
prev_line_start -= 1;
}
// Check if the line is only whitespace
let prev_line = &self.source[prev_line_start..line_start - 1];
if prev_line.iter().all(|&b| b == b' ' || b == b'\t') {
line_start = prev_line_start;
}
}
// Find the end of the line (including newline)
let mut line_end = self.start_byte;
while line_end < self.source.len() && self.source[line_end] != b'\n' {
line_end += 1;
}
// Include the newline character
if line_end < self.source.len() && self.source[line_end] == b'\n' {
line_end += 1;
}
(line_start, line_end)
}
/// Find the start and end byte positions of the line containing this
/// location, including any following blank line
/// Returns (line_start_byte, line_end_byte) including newlines
pub fn find_line_bounds_with_following_blank(&self) -> (usize, usize) {
let line_start = self.find_line_start();
let mut line_end = self.find_line_end();
// Check if the next line is blank (only whitespace)
if line_end < self.source.len() {
let mut pos = line_end;
let mut is_blank = true;
while pos < self.source.len() && self.source[pos] != b'\n' {
if !self.source[pos].is_ascii_whitespace() {
is_blank = false;
break;
}
pos += 1;
}
// If next line is blank, include it in the removal
if is_blank && pos < self.source.len() {
line_end = pos + 1; // Include the newline of the blank line
}
}
(line_start, line_end)
}
/// Extract indentation (leading whitespace) from the line containing this
/// location. Returns all leading spaces/tabs from the start of the line.
pub fn extract_line_indentation(&self) -> String {
let line_start = self.find_line_start();
// Extract all leading whitespace from the line
let mut indent = String::new();
let mut i = line_start;
while i < self.source.len() && (self.source[i] == b' ' || self.source[i] == b'\t') {
indent.push(self.source[i] as char);
i += 1;
}
indent
}
/// Extract indentation (leading whitespace) up to this location
/// Returns the spaces/tabs from line start up to (but not past) the
/// location
pub fn extract_indentation(&self) -> String {
let line_start = self.find_line_start();
// Extract indentation (spaces/tabs before first non-whitespace or before
// location)
let mut indent = String::new();
for &byte in &self.source[line_start..self.start_byte] {
if byte == b' ' || byte == b'\t' {
indent.push(byte as char);
} else {
break;
}
}
indent
}
/// Scan forward from `end_byte` through whitespace to find `target` and
/// return the byte position immediately after it. Returns `end_byte`
/// unchanged if the target is not found before a non-whitespace byte.
pub fn find_after(&self, target: u8) -> usize {
let mut pos = self.end_byte;
while pos < self.source.len() {
if self.source[pos] == target {
return pos + 1;
}
if !self.source[pos].is_ascii_whitespace() {
break;
}
pos += 1;
}
self.end_byte
}
/// Scan backward from `start_byte` through whitespace to find `target` and
/// return the byte position of the target. Returns `start_byte`
/// unchanged if the target is not found before a non-whitespace byte.
pub fn find_before(&self, target: u8) -> usize {
let mut pos = self.start_byte;
while pos > 0 && self.source[pos - 1].is_ascii_whitespace() {
pos -= 1;
}
if pos > 0 && self.source[pos - 1] == target {
pos -= 1;
// Also skip whitespace before the target
while pos > 0 && self.source[pos - 1] == b' ' {
pos -= 1;
}
}
pos
}
/// Convenience: scan forward for `;` — equivalent to `find_after(b';')`
pub fn find_semicolon_end(&self) -> usize {
self.find_after(b';')
}
/// Count consecutive newlines immediately after `end_byte`.
pub fn count_trailing_newlines(&self) -> usize {
self.source[self.end_byte..]
.iter()
.take_while(|&&b| b == b'\n')
.count()
}
/// Find braces surrounding this location in the source.
/// Returns (opening_brace_pos, closing_brace_pos) using depth tracking to
/// find matching braces.
pub fn find_braces_around(&self) -> (usize, usize) {
let source = &self.source;
let start = self.start_byte;
// Search backwards from start to find '{'
let mut brace_start = start;
while brace_start > 0 && source[brace_start - 1] != b'{' {
brace_start -= 1;
if start - brace_start > 100 {
break;
}
}
if brace_start > 0 && source[brace_start - 1] == b'{' {
brace_start -= 1;
}
// Search forwards from opening brace to find matching closing brace using depth
// tracking
let mut brace_end = brace_start + 1;
let mut depth = 1;
while brace_end < source.len() && depth > 0 {
if source[brace_end] == b'{' {
depth += 1;
} else if source[brace_end] == b'}' {
depth -= 1;
}
brace_end += 1;
}
(brace_start, brace_end)
}
}