1use crate::Config;
4use std::path::Path;
5
6pub struct LineIndex {
11 line_starts: Vec<usize>,
13}
14
15impl LineIndex {
16 pub fn new(source: &str) -> Self {
21 let mut line_starts = vec![0]; for (i, c) in source.char_indices() {
24 if c == '\n' {
25 line_starts.push(i + 1);
27 }
28 }
29
30 Self { line_starts }
31 }
32
33 pub fn line_col(&self, offset: usize) -> (usize, usize) {
40 let line_idx = self
44 .line_starts
45 .partition_point(|&start| start <= offset)
46 .saturating_sub(1);
47
48 let line = line_idx + 1; let line_start = self.line_starts[line_idx];
50 let column = offset.saturating_sub(line_start) + 1; (line, column)
53 }
54
55 pub fn line_start(&self, line: usize) -> Option<usize> {
57 self.line_starts.get(line.saturating_sub(1)).copied()
58 }
59
60 pub fn line_count(&self) -> usize {
62 self.line_starts.len()
63 }
64
65 pub fn byte_offset(&self, line: usize, column: usize) -> Option<usize> {
70 let line_start = self.line_start(line)?;
71 line_start.checked_add(column.saturating_sub(1))
74 }
75}
76
77pub struct AnalysisContext<'a> {
81 pub file_path: &'a Path,
82 pub source: &'a str,
83 pub ast: &'a syn::File,
84 pub config: &'a Config,
85 line_index: LineIndex,
86}
87
88impl<'a> AnalysisContext<'a> {
89 pub fn new(
91 file_path: &'a Path,
92 source: &'a str,
93 ast: &'a syn::File,
94 config: &'a Config,
95 ) -> Self {
96 Self {
97 file_path,
98 source,
99 ast,
100 config,
101 line_index: LineIndex::new(source),
102 }
103 }
104
105 #[inline]
109 pub fn line_col(&self, offset: usize) -> (usize, usize) {
110 self.line_index.line_col(offset)
111 }
112
113 pub fn get_line(&self, line_num: usize) -> Option<&str> {
115 self.source.lines().nth(line_num.saturating_sub(1))
116 }
117
118 pub fn line_index(&self) -> &LineIndex {
120 &self.line_index
121 }
122
123 pub fn span_to_byte_range(&self, span: proc_macro2::Span) -> Option<(usize, usize)> {
127 let start = span.start();
128 let end = span.end();
129
130 let start_byte = self.line_index.byte_offset(start.line, start.column + 1)?;
131 let end_byte = self.line_index.byte_offset(end.line, end.column + 1)?;
132
133 Some((start_byte, end_byte))
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn test_line_index_simple() {
143 let source = "line1\nline2\nline3";
144 let index = LineIndex::new(source);
145
146 assert_eq!(index.line_count(), 3);
147
148 assert_eq!(index.line_col(0), (1, 1)); assert_eq!(index.line_col(4), (1, 5)); assert_eq!(index.line_col(6), (2, 1)); assert_eq!(index.line_col(10), (2, 5)); assert_eq!(index.line_col(12), (3, 1)); }
159
160 #[test]
161 fn test_line_index_empty() {
162 let source = "";
163 let index = LineIndex::new(source);
164
165 assert_eq!(index.line_count(), 1);
166 assert_eq!(index.line_col(0), (1, 1));
167 }
168
169 #[test]
170 fn test_line_index_single_line() {
171 let source = "hello world";
172 let index = LineIndex::new(source);
173
174 assert_eq!(index.line_count(), 1);
175 assert_eq!(index.line_col(0), (1, 1));
176 assert_eq!(index.line_col(5), (1, 6));
177 assert_eq!(index.line_col(10), (1, 11));
178 }
179
180 #[test]
181 fn test_line_index_trailing_newline() {
182 let source = "line1\nline2\n";
183 let index = LineIndex::new(source);
184
185 assert_eq!(index.line_count(), 3); assert_eq!(index.line_col(12), (3, 1)); }
188
189 #[test]
190 fn test_line_index_unicode() {
191 let source = "héllo\nwörld";
192 let index = LineIndex::new(source);
193
194 assert_eq!(index.line_col(0), (1, 1)); assert_eq!(index.line_col(7), (2, 1)); }
199
200 #[test]
201 fn test_line_start() {
202 let source = "line1\nline2\nline3";
203 let index = LineIndex::new(source);
204
205 assert_eq!(index.line_start(1), Some(0));
206 assert_eq!(index.line_start(2), Some(6));
207 assert_eq!(index.line_start(3), Some(12));
208 assert_eq!(index.line_start(4), None);
209 }
210
211 #[test]
212 fn test_byte_offset_overflow_protection() {
213 let source = "hello";
214 let index = LineIndex::new(source);
215
216 assert_eq!(index.byte_offset(1, 1), Some(0));
218 assert_eq!(index.byte_offset(1, 3), Some(2));
219
220 let result = index.byte_offset(1, usize::MAX);
223 assert!(result.is_some()); assert_eq!(index.byte_offset(1, 0), Some(0));
227 }
228
229 #[test]
230 fn test_byte_offset_invalid_line() {
231 let source = "hello";
232 let index = LineIndex::new(source);
233
234 assert_eq!(index.byte_offset(0, 1), Some(0));
236
237 assert_eq!(index.byte_offset(100, 1), None);
239 }
240
241 #[test]
242 fn test_span_to_byte_range() {
243 use crate::Config;
244
245 let source = "fn foo() {}";
247 let ast = syn::parse_file(source).expect("Failed to parse");
248 let config = Config::default();
249 let ctx = AnalysisContext::new(std::path::Path::new("test.rs"), source, &ast, &config);
250
251 if let syn::Item::Fn(item_fn) = &ast.items[0] {
253 let ident_span = item_fn.sig.ident.span();
254 let (start, end) = ctx.span_to_byte_range(ident_span).unwrap();
255
256 assert_eq!(start, 3, "Start byte should be 3");
258 assert_eq!(end, 6, "End byte should be 6");
259
260 let extracted = &source[start..end];
262 assert_eq!(extracted, "foo", "Extracted text should be 'foo'");
263 } else {
264 panic!("Expected a function item");
265 }
266 }
267
268 #[test]
269 fn test_span_to_byte_range_multiline() {
270 use crate::Config;
271
272 let source = "fn test() {\n let x = 1;\n}";
274 let ast = syn::parse_file(source).expect("Failed to parse");
275 let config = Config::default();
276 let ctx = AnalysisContext::new(std::path::Path::new("test.rs"), source, &ast, &config);
277
278 if let syn::Item::Fn(item_fn) = &ast.items[0] {
280 if let syn::Stmt::Local(local) = &item_fn.block.stmts[0] {
281 if let syn::Pat::Ident(pat_ident) = &local.pat {
282 let ident_span = pat_ident.ident.span();
283 let (start, end) = ctx.span_to_byte_range(ident_span).unwrap();
284
285 let extracted = &source[start..end];
287 assert_eq!(extracted, "x", "Extracted text should be 'x'");
288 }
289 }
290 }
291 }
292}