entrenar/citl/trainer/
span.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
7pub struct SourceSpan {
8 pub file: String,
10 pub start_line: u32,
12 pub start_col: u32,
14 pub end_line: u32,
16 pub end_col: u32,
18}
19
20impl SourceSpan {
21 #[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 #[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 #[must_use]
41 pub fn overlaps(&self, other: &Self) -> bool {
42 if self.file != other.file {
43 return false;
44 }
45
46 !(self.end_line < other.start_line || other.end_line < self.start_line)
48 }
49
50 #[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}