php_parser/
span.rs

1use serde::Serialize;
2use std::cell::RefCell;
3use std::fmt;
4
5thread_local! {
6    static DEBUG_SOURCE: RefCell<Option<&'static [u8]>> = const { RefCell::new(None) };
7}
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct LineInfo<'src> {
11    pub line: usize,
12    pub column: usize,
13    pub line_text: &'src [u8],
14}
15
16/// Execute a closure with a source code context for Span debugging.
17/// This allows Spans to print their line number and text content when Debug formatted.
18pub fn with_session_globals<F, R>(source: &[u8], f: F) -> R
19where
20    F: FnOnce() -> R,
21{
22    // SAFETY: We are extending the lifetime of the slice to 'static to store it in a thread_local.
23    // We ensure that the thread_local is cleared before this function returns, so the reference
24    // never outlives the actual data.
25    let source_static: &'static [u8] = unsafe { std::mem::transmute(source) };
26
27    DEBUG_SOURCE.with(|s| *s.borrow_mut() = Some(source_static));
28    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
29    DEBUG_SOURCE.with(|s| *s.borrow_mut() = None);
30
31    match result {
32        Ok(r) => r,
33        Err(e) => std::panic::resume_unwind(e),
34    }
35}
36
37#[derive(Clone, Copy, PartialEq, Eq, Default, Hash, Serialize)]
38pub struct Span {
39    pub start: usize,
40    pub end: usize,
41}
42
43impl fmt::Debug for Span {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        let mut builder = f.debug_struct("Span");
46        builder.field("start", &self.start);
47        builder.field("end", &self.end);
48
49        DEBUG_SOURCE.with(|source_cell| {
50            if let Some(source) = *source_cell.borrow()
51                && self.start <= self.end
52                && self.end <= source.len()
53            {
54                let line = source[..self.start].iter().filter(|&&b| b == b'\n').count() + 1;
55                builder.field("line", &line);
56
57                let text = &source[self.start..self.end];
58                let text_str = String::from_utf8_lossy(text);
59                builder.field("text", &text_str);
60            }
61        });
62
63        builder.finish()
64    }
65}
66
67impl Span {
68    pub fn new(start: usize, end: usize) -> Self {
69        Self { start, end }
70    }
71
72    pub fn len(&self) -> usize {
73        self.end - self.start
74    }
75
76    pub fn is_empty(&self) -> bool {
77        self.start == self.end
78    }
79
80    pub fn line_info<'src>(&self, source: &'src [u8]) -> Option<LineInfo<'src>> {
81        if self.start > self.end || self.end > source.len() {
82            return None;
83        }
84
85        let line = source[..self.start].iter().filter(|&&b| b == b'\n').count() + 1;
86        let line_start = source[..self.start]
87            .iter()
88            .rposition(|b| *b == b'\n')
89            .map(|pos| pos + 1)
90            .unwrap_or(0);
91        let column = self.start - line_start + 1;
92
93        let line_end = source[self.start..]
94            .iter()
95            .position(|b| *b == b'\n')
96            .map(|pos| self.start + pos)
97            .unwrap_or(source.len());
98
99        Some(LineInfo {
100            line,
101            column,
102            line_text: &source[line_start..line_end],
103        })
104    }
105
106    /// Safely slice the source. Returns None if indices are out of bounds.
107    /// In this project we assume the parser manages bounds correctly, but for safety we could return Option.
108    /// For now, following ARCHITECTURE.md, we return slice.
109    pub fn as_str<'src>(&self, source: &'src [u8]) -> &'src [u8] {
110        &source[self.start..self.end]
111    }
112}