1use std::{ops::Range, path::Path, sync::Arc};
19
20use serde::{Deserialize, Serialize};
21
22use crate::error::ValidationError;
23
24#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[non_exhaustive]
33#[serde(rename_all = "camelCase")]
34pub struct Span {
35 #[serde(with = "crate::ir::path_serde::arc_path")]
37 pub file: Arc<Path>,
38
39 pub byte_range: Range<u32>,
41
42 pub line: u32,
44
45 pub column: u32,
47}
48
49impl Span {
50 pub fn new(
57 file: Arc<Path>,
58 byte_range: Range<u32>,
59 line: u32,
60 column: u32,
61 ) -> Result<Self, ValidationError> {
62 if byte_range.start > byte_range.end {
63 return Err(ValidationError::Shape {
64 field: "Span.byte_range",
65 rule: "start-after-end",
66 });
67 }
68 if line == 0 {
69 return Err(ValidationError::Shape {
70 field: "Span.line",
71 rule: "must-be-1-based",
72 });
73 }
74 if column == 0 {
75 return Err(ValidationError::Shape {
76 field: "Span.column",
77 rule: "must-be-1-based",
78 });
79 }
80 Ok(Self {
81 file,
82 byte_range,
83 line,
84 column,
85 })
86 }
87
88 #[must_use]
91 pub fn synthetic() -> Self {
92 Self {
93 file: Arc::from(Path::new("")),
94 byte_range: 0..0,
95 line: 1,
96 column: 1,
97 }
98 }
99
100 #[must_use]
103 pub fn byte_range_usize(&self) -> Range<usize> {
104 self.byte_range.start as usize..self.byte_range.end as usize
105 }
106}
107
108#[cfg(test)]
109#[allow(
110 clippy::unwrap_used,
111 clippy::expect_used,
112 clippy::panic,
113 clippy::indexing_slicing,
114 clippy::reversed_empty_ranges
115)]
116mod tests {
117 use super::*;
118
119 fn p(s: &str) -> Arc<Path> {
120 Arc::from(Path::new(s))
121 }
122
123 fn must_err(r: Result<Span, ValidationError>) -> ValidationError {
124 match r {
125 Ok(s) => panic!("expected Err, got Ok({s:?})"),
126 Err(e) => e,
127 }
128 }
129
130 #[test]
131 fn test_should_construct_span_with_valid_inputs() {
132 let s = Span::new(p("/tmp/x.tf"), 10..20, 3, 5).unwrap();
133 assert_eq!(s.byte_range, 10..20);
134 assert_eq!(s.line, 3);
135 assert_eq!(s.column, 5);
136 assert_eq!(s.byte_range_usize(), 10usize..20usize);
137 }
138
139 #[test]
140 fn test_should_reject_reversed_byte_range() {
141 let range = Range::<u32> { start: 30, end: 10 };
145 let err = must_err(Span::new(p("/tmp/x.tf"), range, 1, 1));
146 assert!(matches!(
147 err,
148 ValidationError::Shape {
149 rule: "start-after-end",
150 ..
151 }
152 ));
153 }
154
155 #[test]
156 fn test_should_reject_zero_line() {
157 let err = must_err(Span::new(p("/tmp/x.tf"), 0..1, 0, 1));
158 assert!(matches!(
159 err,
160 ValidationError::Shape {
161 field: "Span.line",
162 ..
163 }
164 ));
165 }
166
167 #[test]
168 fn test_should_reject_zero_column() {
169 let err = must_err(Span::new(p("/tmp/x.tf"), 0..1, 1, 0));
170 assert!(matches!(
171 err,
172 ValidationError::Shape {
173 field: "Span.column",
174 ..
175 }
176 ));
177 }
178
179 #[test]
180 fn test_should_serde_round_trip_span() {
181 let s = Span::new(p("foo/bar.tf"), 5..15, 2, 4).unwrap();
182 let json = serde_json::to_string(&s).unwrap();
183 let back: Span = serde_json::from_str(&json).unwrap();
184 assert_eq!(s, back);
185 }
186
187 #[test]
188 fn test_should_serialize_span_in_camel_case() {
189 let s = Span::new(p("a.tf"), 0..1, 1, 1).unwrap();
190 let json = serde_json::to_string(&s).unwrap();
191 assert!(json.contains("byteRange"), "got: {json}");
192 }
193
194 #[test]
195 fn test_should_serde_round_trip_synthetic_span() {
196 let s = Span::synthetic();
197 let json = serde_json::to_string(&s).unwrap();
198 assert!(
199 json.contains("\"file\":\"\""),
200 "expected empty file: {json}"
201 );
202 assert!(json.contains("\"line\":1"), "{json}");
203 let back: Span = serde_json::from_str(&json).unwrap();
204 assert_eq!(s, back);
205 }
206}