Skip to main content

hotspots_core/language/
span.rs

1//! Language-agnostic source span representation
2
3use serde::{Deserialize, Serialize};
4
5/// Language-agnostic source code span
6///
7/// Represents a contiguous region of source code, independent of the parser used.
8/// All parsers must convert their native span types to this representation.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct SourceSpan {
11    /// Byte offset of the start of the span (inclusive)
12    pub start: usize,
13    /// Byte offset of the end of the span (exclusive)
14    pub end: usize,
15    /// Line number of the start (1-indexed)
16    pub start_line: u32,
17    /// Line number of the end (1-indexed)
18    pub end_line: u32,
19    /// Column number of the start (0-indexed, in bytes)
20    pub start_col: u32,
21}
22
23impl SourceSpan {
24    /// Create a new source span
25    pub fn new(start: usize, end: usize, start_line: u32, end_line: u32, start_col: u32) -> Self {
26        SourceSpan {
27            start,
28            end,
29            start_line,
30            end_line,
31            start_col,
32        }
33    }
34
35    /// Get the length of the span in bytes
36    pub fn len(&self) -> usize {
37        self.end.saturating_sub(self.start)
38    }
39
40    /// Check if the span is empty
41    pub fn is_empty(&self) -> bool {
42        self.start >= self.end
43    }
44
45    /// Check if this span contains another span
46    pub fn contains(&self, other: &SourceSpan) -> bool {
47        self.start <= other.start && other.end <= self.end
48    }
49
50    /// Check if this span overlaps with another span
51    ///
52    /// Zero-width spans (where start == end) don't overlap with anything,
53    /// including themselves.
54    pub fn overlaps(&self, other: &SourceSpan) -> bool {
55        // Zero-width spans don't overlap
56        if self.is_empty() || other.is_empty() {
57            return false;
58        }
59        self.start < other.end && other.start < self.end
60    }
61}
62
63/// Convert from SWC Span to SourceSpan
64impl From<swc_common::Span> for SourceSpan {
65    fn from(span: swc_common::Span) -> Self {
66        // Note: We can't get line/col from a bare Span without SourceMap
67        // This will be handled by the parser which has access to SourceMap
68        SourceSpan {
69            start: span.lo.0 as usize,
70            end: span.hi.0 as usize,
71            start_line: 0, // To be filled in by parser
72            end_line: 0,   // To be filled in by parser
73            start_col: 0,  // To be filled in by parser
74        }
75    }
76}
77
78/// Helper to convert SWC Span to SourceSpan with line/column info
79pub fn span_with_location(
80    span: swc_common::Span,
81    source_map: &swc_common::SourceMap,
82) -> SourceSpan {
83    let start_loc = source_map.lookup_char_pos(span.lo);
84    let end_loc = source_map.lookup_char_pos(span.hi);
85    SourceSpan {
86        start: span.lo.0 as usize,
87        end: span.hi.0 as usize,
88        start_line: start_loc.line as u32,
89        end_line: end_loc.line as u32,
90        start_col: start_loc.col.0 as u32,
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_new() {
100        let span = SourceSpan::new(10, 20, 1, 3, 5);
101        assert_eq!(span.start, 10);
102        assert_eq!(span.end, 20);
103        assert_eq!(span.start_line, 1);
104        assert_eq!(span.end_line, 3);
105        assert_eq!(span.start_col, 5);
106    }
107
108    #[test]
109    fn test_len() {
110        let span = SourceSpan::new(10, 20, 1, 3, 5);
111        assert_eq!(span.len(), 10);
112
113        let empty_span = SourceSpan::new(10, 10, 1, 1, 5);
114        assert_eq!(empty_span.len(), 0);
115    }
116
117    #[test]
118    fn test_is_empty() {
119        let span = SourceSpan::new(10, 20, 1, 3, 5);
120        assert!(!span.is_empty());
121
122        let empty_span = SourceSpan::new(10, 10, 1, 1, 5);
123        assert!(empty_span.is_empty());
124
125        let backwards_span = SourceSpan::new(20, 10, 1, 1, 5);
126        assert!(backwards_span.is_empty());
127    }
128
129    #[test]
130    fn test_contains() {
131        let outer = SourceSpan::new(10, 30, 1, 5, 5);
132        let inner = SourceSpan::new(15, 25, 2, 4, 10);
133        let outside = SourceSpan::new(5, 15, 1, 2, 0);
134
135        assert!(outer.contains(&inner));
136        assert!(!inner.contains(&outer));
137        assert!(!outer.contains(&outside));
138
139        // Span contains itself
140        assert!(outer.contains(&outer));
141    }
142
143    #[test]
144    fn test_overlaps() {
145        let span1 = SourceSpan::new(10, 20, 1, 3, 5);
146        let span2 = SourceSpan::new(15, 25, 2, 4, 10);
147        let span3 = SourceSpan::new(25, 30, 4, 5, 20);
148
149        assert!(span1.overlaps(&span2));
150        assert!(span2.overlaps(&span1));
151        assert!(!span1.overlaps(&span3));
152        assert!(!span3.overlaps(&span1));
153
154        // Non-empty span overlaps with itself
155        assert!(span1.overlaps(&span1));
156    }
157
158    #[test]
159    fn test_edge_cases() {
160        // Adjacent spans don't overlap
161        let span1 = SourceSpan::new(10, 20, 1, 3, 5);
162        let span2 = SourceSpan::new(20, 30, 3, 5, 15);
163        assert!(!span1.overlaps(&span2));
164
165        // Zero-width span
166        let zero_span = SourceSpan::new(15, 15, 2, 2, 10);
167        assert!(span1.contains(&zero_span));
168        assert!(!zero_span.overlaps(&span1)); // Zero-width doesn't overlap
169    }
170}