Skip to main content

gobject_ast/model/
source_location.rs

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