Skip to main content

entrenar/citl/trainer/
span.rs

1//! Source span types for CITL trainer
2
3use serde::{Deserialize, Serialize};
4
5/// A source code span (location in source file)
6#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
7pub struct SourceSpan {
8    /// File path
9    pub file: String,
10    /// Start line (1-indexed)
11    pub start_line: u32,
12    /// Start column (1-indexed)
13    pub start_col: u32,
14    /// End line (1-indexed)
15    pub end_line: u32,
16    /// End column (1-indexed)
17    pub end_col: u32,
18}
19
20impl SourceSpan {
21    /// Create a new source span
22    #[must_use]
23    pub fn new(
24        file: impl Into<String>,
25        start_line: u32,
26        start_col: u32,
27        end_line: u32,
28        end_col: u32,
29    ) -> Self {
30        Self { file: file.into(), start_line, start_col, end_line, end_col }
31    }
32
33    /// Create a single-line span
34    #[must_use]
35    pub fn line(file: impl Into<String>, line: u32) -> Self {
36        Self::new(file, line, 1, line, u32::MAX)
37    }
38
39    /// Check if this span overlaps with another
40    #[must_use]
41    pub fn overlaps(&self, other: &Self) -> bool {
42        if self.file != other.file {
43            return false;
44        }
45
46        // Check if ranges overlap
47        !(self.end_line < other.start_line || other.end_line < self.start_line)
48    }
49
50    /// Check if this span contains another
51    #[must_use]
52    pub fn contains(&self, other: &Self) -> bool {
53        if self.file != other.file {
54            return false;
55        }
56
57        self.start_line <= other.start_line && self.end_line >= other.end_line
58    }
59}
60
61impl std::fmt::Display for SourceSpan {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        write!(
64            f,
65            "{}:{}:{}-{}:{}",
66            self.file, self.start_line, self.start_col, self.end_line, self.end_col
67        )
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_source_span_new() {
77        let span = SourceSpan::new("main.rs", 1, 1, 10, 80);
78        assert_eq!(span.file, "main.rs");
79        assert_eq!(span.start_line, 1);
80        assert_eq!(span.end_line, 10);
81    }
82
83    #[test]
84    fn test_source_span_line() {
85        let span = SourceSpan::line("main.rs", 5);
86        assert_eq!(span.file, "main.rs");
87        assert_eq!(span.start_line, 5);
88        assert_eq!(span.end_line, 5);
89    }
90
91    #[test]
92    fn test_source_span_overlaps_same_line() {
93        let span1 = SourceSpan::line("main.rs", 5);
94        let span2 = SourceSpan::line("main.rs", 5);
95        assert!(span1.overlaps(&span2));
96    }
97
98    #[test]
99    fn test_source_span_overlaps_different_lines() {
100        let span1 = SourceSpan::new("main.rs", 1, 1, 10, 80);
101        let span2 = SourceSpan::new("main.rs", 5, 1, 15, 80);
102        assert!(span1.overlaps(&span2));
103    }
104
105    #[test]
106    fn test_source_span_no_overlap() {
107        let span1 = SourceSpan::new("main.rs", 1, 1, 5, 80);
108        let span2 = SourceSpan::new("main.rs", 10, 1, 15, 80);
109        assert!(!span1.overlaps(&span2));
110    }
111
112    #[test]
113    fn test_source_span_no_overlap_different_files() {
114        let span1 = SourceSpan::line("main.rs", 5);
115        let span2 = SourceSpan::line("lib.rs", 5);
116        assert!(!span1.overlaps(&span2));
117    }
118
119    #[test]
120    fn test_source_span_contains() {
121        let outer = SourceSpan::new("main.rs", 1, 1, 20, 80);
122        let inner = SourceSpan::new("main.rs", 5, 1, 10, 80);
123        assert!(outer.contains(&inner));
124        assert!(!inner.contains(&outer));
125    }
126
127    #[test]
128    fn test_source_span_display() {
129        let span = SourceSpan::new("main.rs", 5, 10, 5, 20);
130        let display = format!("{span}");
131        assert!(display.contains("main.rs"));
132        assert!(display.contains('5'));
133    }
134
135    #[test]
136    fn test_source_span_serialization() {
137        let span = SourceSpan::line("main.rs", 5);
138        let json = serde_json::to_string(&span).expect("JSON serialization should succeed");
139        let deserialized: SourceSpan =
140            serde_json::from_str(&json).expect("JSON deserialization should succeed");
141        assert_eq!(span, deserialized);
142    }
143}
144
145#[cfg(test)]
146mod prop_tests {
147    use super::*;
148    use proptest::prelude::*;
149
150    proptest! {
151        #[test]
152        fn prop_source_span_overlap_symmetric(
153            line1 in 1u32..100,
154            line2 in 1u32..100
155        ) {
156            let span1 = SourceSpan::line("file.rs", line1);
157            let span2 = SourceSpan::line("file.rs", line2);
158
159            prop_assert_eq!(span1.overlaps(&span2), span2.overlaps(&span1));
160        }
161    }
162}