Skip to main content

hedl_lint/fix/
range.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Source position and range utilities for fix application
19
20/// Source position with line and column
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct SourcePosition {
23    /// Line number (1-indexed)
24    pub line: usize,
25    /// Column offset in bytes (0-indexed)
26    pub column: usize,
27}
28
29impl SourcePosition {
30    /// Create a new source position
31    #[must_use]
32    pub fn new(line: usize, column: usize) -> Self {
33        Self { line, column }
34    }
35
36    /// Create a position at the start of a file
37    #[must_use]
38    pub fn start() -> Self {
39        Self { line: 1, column: 0 }
40    }
41
42    /// Create a position at the end of a file (max values)
43    #[must_use]
44    pub fn end() -> Self {
45        Self {
46            line: usize::MAX,
47            column: usize::MAX,
48        }
49    }
50}
51
52/// Source range for fix application
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct SourceRange {
55    /// Start position (inclusive)
56    pub start: SourcePosition,
57    /// End position (exclusive)
58    pub end: SourcePosition,
59}
60
61impl SourceRange {
62    /// Create a new source range
63    #[must_use]
64    pub fn new(start: SourcePosition, end: SourcePosition) -> Self {
65        Self { start, end }
66    }
67
68    /// Create a range spanning an entire line
69    #[must_use]
70    pub fn line(line: usize) -> Self {
71        Self {
72            start: SourcePosition::new(line, 0),
73            end: SourcePosition::new(line + 1, 0),
74        }
75    }
76
77    /// Create a single-point range (empty)
78    #[must_use]
79    pub fn point(pos: SourcePosition) -> Self {
80        Self {
81            start: pos,
82            end: pos,
83        }
84    }
85
86    /// Check if this range overlaps with another
87    #[must_use]
88    pub fn overlaps(&self, other: &SourceRange) -> bool {
89        // Ranges overlap if they are not completely disjoint
90        !(self.end <= other.start || other.end <= self.start)
91    }
92
93    /// Check if this range contains another range
94    #[must_use]
95    pub fn contains(&self, other: &SourceRange) -> bool {
96        self.start <= other.start && other.end <= self.end
97    }
98
99    /// Check if this range contains a position
100    #[must_use]
101    pub fn contains_position(&self, pos: &SourcePosition) -> bool {
102        self.start <= *pos && *pos < self.end
103    }
104
105    /// Check if this range is empty (start == end)
106    #[must_use]
107    pub fn is_empty(&self) -> bool {
108        self.start == self.end
109    }
110
111    /// Merge two ranges into the smallest range containing both
112    #[must_use]
113    pub fn merge(&self, other: &SourceRange) -> SourceRange {
114        SourceRange {
115            start: self.start.min(other.start),
116            end: self.end.max(other.end),
117        }
118    }
119
120    /// Get the length in lines
121    #[must_use]
122    pub fn line_count(&self) -> usize {
123        self.end.line.saturating_sub(self.start.line)
124    }
125
126    /// Check if this is a valid range (start <= end)
127    #[must_use]
128    pub fn is_valid(&self) -> bool {
129        self.start <= self.end
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_source_position_new() {
139        let pos = SourcePosition::new(42, 10);
140        assert_eq!(pos.line, 42);
141        assert_eq!(pos.column, 10);
142    }
143
144    #[test]
145    fn test_source_position_start() {
146        let pos = SourcePosition::start();
147        assert_eq!(pos.line, 1);
148        assert_eq!(pos.column, 0);
149    }
150
151    #[test]
152    fn test_source_position_end() {
153        let pos = SourcePosition::end();
154        assert_eq!(pos.line, usize::MAX);
155        assert_eq!(pos.column, usize::MAX);
156    }
157
158    #[test]
159    fn test_source_position_ordering() {
160        let pos1 = SourcePosition::new(1, 5);
161        let pos2 = SourcePosition::new(1, 10);
162        let pos3 = SourcePosition::new(2, 0);
163
164        assert!(pos1 < pos2);
165        assert!(pos2 < pos3);
166        assert!(pos1 < pos3);
167    }
168
169    #[test]
170    fn test_source_position_equality() {
171        let pos1 = SourcePosition::new(5, 10);
172        let pos2 = SourcePosition::new(5, 10);
173        let pos3 = SourcePosition::new(5, 11);
174
175        assert_eq!(pos1, pos2);
176        assert_ne!(pos1, pos3);
177    }
178
179    #[test]
180    fn test_source_range_new() {
181        let start = SourcePosition::new(1, 0);
182        let end = SourcePosition::new(1, 10);
183        let range = SourceRange::new(start, end);
184
185        assert_eq!(range.start, start);
186        assert_eq!(range.end, end);
187    }
188
189    #[test]
190    fn test_source_range_line() {
191        let range = SourceRange::line(5);
192        assert_eq!(range.start.line, 5);
193        assert_eq!(range.start.column, 0);
194        assert_eq!(range.end.line, 6);
195        assert_eq!(range.end.column, 0);
196    }
197
198    #[test]
199    fn test_source_range_point() {
200        let pos = SourcePosition::new(3, 7);
201        let range = SourceRange::point(pos);
202        assert_eq!(range.start, pos);
203        assert_eq!(range.end, pos);
204        assert!(range.is_empty());
205    }
206
207    #[test]
208    fn test_range_overlaps_true() {
209        let range1 = SourceRange::new(SourcePosition::new(1, 0), SourcePosition::new(1, 10));
210        let range2 = SourceRange::new(SourcePosition::new(1, 5), SourcePosition::new(1, 15));
211
212        assert!(range1.overlaps(&range2));
213        assert!(range2.overlaps(&range1));
214    }
215
216    #[test]
217    fn test_range_overlaps_false() {
218        let range1 = SourceRange::new(SourcePosition::new(1, 0), SourcePosition::new(1, 5));
219        let range2 = SourceRange::new(SourcePosition::new(1, 10), SourcePosition::new(1, 15));
220
221        assert!(!range1.overlaps(&range2));
222        assert!(!range2.overlaps(&range1));
223    }
224
225    #[test]
226    fn test_range_overlaps_adjacent() {
227        let range1 = SourceRange::new(SourcePosition::new(1, 0), SourcePosition::new(1, 5));
228        let range2 = SourceRange::new(SourcePosition::new(1, 5), SourcePosition::new(1, 10));
229
230        // Adjacent ranges don't overlap (end is exclusive)
231        assert!(!range1.overlaps(&range2));
232    }
233
234    #[test]
235    fn test_range_overlaps_contained() {
236        let range1 = SourceRange::new(SourcePosition::new(1, 0), SourcePosition::new(1, 20));
237        let range2 = SourceRange::new(SourcePosition::new(1, 5), SourcePosition::new(1, 15));
238
239        assert!(range1.overlaps(&range2));
240        assert!(range2.overlaps(&range1));
241    }
242
243    #[test]
244    fn test_range_contains_range() {
245        let outer = SourceRange::new(SourcePosition::new(1, 0), SourcePosition::new(3, 0));
246        let inner = SourceRange::new(SourcePosition::new(2, 0), SourcePosition::new(2, 10));
247
248        assert!(outer.contains(&inner));
249        assert!(!inner.contains(&outer));
250    }
251
252    #[test]
253    fn test_range_contains_position() {
254        let range = SourceRange::new(SourcePosition::new(1, 5), SourcePosition::new(1, 15));
255
256        assert!(range.contains_position(&SourcePosition::new(1, 5)));
257        assert!(range.contains_position(&SourcePosition::new(1, 10)));
258        assert!(!range.contains_position(&SourcePosition::new(1, 15))); // end is exclusive
259        assert!(!range.contains_position(&SourcePosition::new(1, 0)));
260        assert!(!range.contains_position(&SourcePosition::new(2, 0)));
261    }
262
263    #[test]
264    fn test_range_is_empty() {
265        let empty = SourceRange::point(SourcePosition::new(1, 5));
266        assert!(empty.is_empty());
267
268        let non_empty = SourceRange::new(SourcePosition::new(1, 5), SourcePosition::new(1, 10));
269        assert!(!non_empty.is_empty());
270    }
271
272    #[test]
273    fn test_range_merge() {
274        let range1 = SourceRange::new(SourcePosition::new(1, 0), SourcePosition::new(1, 10));
275        let range2 = SourceRange::new(SourcePosition::new(1, 5), SourcePosition::new(2, 0));
276
277        let merged = range1.merge(&range2);
278        assert_eq!(merged.start, SourcePosition::new(1, 0));
279        assert_eq!(merged.end, SourcePosition::new(2, 0));
280    }
281
282    #[test]
283    fn test_range_merge_disjoint() {
284        let range1 = SourceRange::new(SourcePosition::new(1, 0), SourcePosition::new(1, 5));
285        let range2 = SourceRange::new(SourcePosition::new(3, 0), SourcePosition::new(3, 5));
286
287        let merged = range1.merge(&range2);
288        assert_eq!(merged.start, SourcePosition::new(1, 0));
289        assert_eq!(merged.end, SourcePosition::new(3, 5));
290    }
291
292    #[test]
293    fn test_range_line_count() {
294        let range = SourceRange::new(SourcePosition::new(1, 0), SourcePosition::new(5, 0));
295        assert_eq!(range.line_count(), 4);
296
297        let single_line = SourceRange::new(SourcePosition::new(3, 5), SourcePosition::new(3, 10));
298        assert_eq!(single_line.line_count(), 0);
299    }
300
301    #[test]
302    fn test_range_is_valid() {
303        let valid = SourceRange::new(SourcePosition::new(1, 0), SourcePosition::new(2, 0));
304        assert!(valid.is_valid());
305
306        let invalid = SourceRange::new(SourcePosition::new(2, 0), SourcePosition::new(1, 0));
307        assert!(!invalid.is_valid());
308    }
309
310    #[test]
311    fn test_range_equality() {
312        let range1 = SourceRange::new(SourcePosition::new(1, 0), SourcePosition::new(1, 10));
313        let range2 = SourceRange::new(SourcePosition::new(1, 0), SourcePosition::new(1, 10));
314        let range3 = SourceRange::new(SourcePosition::new(1, 0), SourcePosition::new(1, 11));
315
316        assert_eq!(range1, range2);
317        assert_ne!(range1, range3);
318    }
319
320    #[test]
321    fn test_multiline_range() {
322        let range = SourceRange::new(SourcePosition::new(1, 5), SourcePosition::new(5, 10));
323
324        assert!(range.is_valid());
325        assert_eq!(range.line_count(), 4);
326        assert!(range.contains_position(&SourcePosition::new(3, 0)));
327        assert!(!range.contains_position(&SourcePosition::new(6, 0)));
328    }
329}