sqruff_lib_core/parser/
markers.rs

1use std::ops::Range;
2use std::rc::Rc;
3
4use ahash::AHashSet;
5
6use crate::slice_helpers::zero_slice;
7use crate::templaters::base::TemplatedFile;
8
9/// A reference to a position in a file.
10///
11/// Things to note:
12/// - This combines the previous functionality of FilePositionMarker and
13///   EnrichedFilePositionMarker. Additionally it contains a reference to the
14///   original templated file.
15/// - It no longer explicitly stores a line number or line position in the
16///   source or template. This is extrapolated from the templated file as
17///   required.
18/// - Positions in the source and template are with slices and therefore
19///   identify ranges.
20/// - Positions within the fixed file are identified with a line number and line
21///   position, which identify a point.
22/// - Arithmetic comparisons are on the location in the fixed file.
23#[derive(Debug, Clone)]
24pub struct PositionMarker {
25    data: Rc<PositionMarkerData>,
26}
27
28impl std::ops::Deref for PositionMarker {
29    type Target = PositionMarkerData;
30
31    fn deref(&self) -> &Self::Target {
32        &self.data
33    }
34}
35
36impl std::ops::DerefMut for PositionMarker {
37    fn deref_mut(&mut self) -> &mut Self::Target {
38        Rc::make_mut(&mut self.data)
39    }
40}
41
42impl Eq for PositionMarker {}
43
44#[derive(Debug, Clone)]
45pub struct PositionMarkerData {
46    pub source_slice: Range<usize>,
47    pub templated_slice: Range<usize>,
48    pub templated_file: TemplatedFile,
49    pub working_line_no: usize,
50    pub working_line_pos: usize,
51}
52
53impl Default for PositionMarker {
54    fn default() -> Self {
55        Self {
56            data: PositionMarkerData {
57                source_slice: 0..0,
58                templated_slice: 0..0,
59                templated_file: "".to_string().into(),
60                working_line_no: 0,
61                working_line_pos: 0,
62            }
63            .into(),
64        }
65    }
66}
67
68impl PositionMarker {
69    /// creates a PositionMarker
70    ///
71    /// Unlike the Python version, post_init is embedded into the new function.
72    pub fn new(
73        source_slice: Range<usize>,
74        templated_slice: Range<usize>,
75        templated_file: TemplatedFile,
76        working_line_no: Option<usize>,
77        working_line_pos: Option<usize>,
78    ) -> Self {
79        match (working_line_no, working_line_pos) {
80            (Some(working_line_no), Some(working_line_pos)) => Self {
81                data: PositionMarkerData {
82                    source_slice,
83                    templated_slice,
84                    templated_file,
85                    working_line_no,
86                    working_line_pos,
87                }
88                .into(),
89            },
90            _ => {
91                let (working_line_no, working_line_pos) =
92                    templated_file.get_line_pos_of_char_pos(templated_slice.start, false);
93                Self {
94                    data: PositionMarkerData {
95                        source_slice,
96                        templated_slice,
97                        templated_file,
98                        working_line_no,
99                        working_line_pos,
100                    }
101                    .into(),
102                }
103            }
104        }
105    }
106
107    #[track_caller]
108    pub fn source_str(&self) -> &str {
109        &self.templated_file.source_str[self.source_slice.clone()]
110    }
111
112    pub fn line_no(&self) -> usize {
113        self.source_position().0
114    }
115
116    pub fn line_pos(&self) -> usize {
117        self.source_position().1
118    }
119
120    #[track_caller]
121    pub fn from_child_markers<'a>(
122        markers: impl Iterator<Item = &'a PositionMarker>,
123    ) -> PositionMarker {
124        let mut source_start = usize::MAX;
125        let mut source_end = usize::MIN;
126        let mut template_start = usize::MAX;
127        let mut template_end = usize::MIN;
128        let mut templated_files = AHashSet::new();
129
130        for marker in markers {
131            source_start = source_start.min(marker.source_slice.start);
132            source_end = source_end.max(marker.source_slice.end);
133            template_start = template_start.min(marker.templated_slice.start);
134            template_end = template_end.max(marker.templated_slice.end);
135            templated_files.insert(marker.templated_file.clone());
136        }
137
138        if templated_files.len() != 1 {
139            panic!("Attempted to make a parent marker from multiple files.");
140        }
141
142        let templated_file = templated_files.into_iter().next().unwrap();
143        PositionMarker::new(
144            source_start..source_end,
145            template_start..template_end,
146            templated_file,
147            None,
148            None,
149        )
150    }
151
152    /// Return the line and position of this marker in the source.
153    pub fn source_position(&self) -> (usize, usize) {
154        self.templated_file
155            .get_line_pos_of_char_pos(self.templated_slice.start, true)
156    }
157
158    /// Return the line and position of this marker in the source.
159    pub fn templated_position(&self) -> (usize, usize) {
160        self.templated_file
161            .get_line_pos_of_char_pos(self.templated_slice.start, false)
162    }
163
164    pub fn working_loc_after(&self, raw: &str) -> (usize, usize) {
165        Self::infer_next_position(raw, self.working_line_no, self.working_line_pos)
166    }
167
168    /// Using the raw string provided to infer the position of the next.
169    /// **Line position in 1-indexed.**
170    pub fn infer_next_position(raw: &str, line_no: usize, line_pos: usize) -> (usize, usize) {
171        if raw.is_empty() {
172            return (line_no, line_pos);
173        }
174        let split: Vec<&str> = raw.split('\n').collect();
175        (
176            line_no + (split.len() - 1),
177            if split.len() == 1 {
178                line_pos + raw.len()
179            } else {
180                split.last().unwrap().len() + 1
181            },
182        )
183    }
184
185    /// Location tuple for the working position.
186    pub fn working_loc(&self) -> (usize, usize) {
187        (self.working_line_no, self.working_line_pos)
188    }
189
190    /// Convenience method for creating point markers.
191    pub fn from_point(
192        source_point: usize,
193        templated_point: usize,
194        templated_file: TemplatedFile,
195        working_line_no: Option<usize>,
196        working_line_pos: Option<usize>,
197    ) -> Self {
198        Self::new(
199            zero_slice(source_point),
200            zero_slice(templated_point),
201            templated_file,
202            working_line_no,
203            working_line_pos,
204        )
205    }
206
207    /// Get a point marker from the start.
208    pub fn start_point_marker(&self) -> PositionMarker {
209        PositionMarker::from_point(
210            self.source_slice.start,
211            self.templated_slice.start,
212            self.templated_file.clone(),
213            // Start points also pass on the working position
214            Some(self.working_line_no),
215            Some(self.working_line_pos),
216        )
217    }
218
219    pub fn end_point_marker(&self) -> PositionMarker {
220        // Assuming PositionMarker is a struct and from_point is an associated function
221        PositionMarker::from_point(
222            self.source_slice.end,
223            self.templated_slice.end,
224            self.templated_file.clone(),
225            None,
226            None,
227        )
228    }
229
230    /// Infer literalness from context.
231    ///
232    /// is_literal should return True if a fix can be applied across
233    /// this area in the templated file while being confident that
234    /// the fix is still appropriate in the source file. This
235    /// obviously applies to any slices which are the same in the
236    /// source and the templated files. Slices which are zero-length
237    /// in the source are also SyntaxKind::Literal because they can't be
238    /// "broken" by any fixes, because they don't exist in the source.
239    /// This includes meta segments and any segments added during
240    /// the fixing process.
241    ///
242    /// This value is used for:
243    ///     - Ignoring linting errors in templated sections.
244    ///     - Whether `iter_patches` can return without recursing.
245    ///     - Whether certain rules (such as JJ01) are triggered.
246    pub fn is_literal(&self) -> bool {
247        self.templated_file
248            .is_source_slice_literal(&self.source_slice)
249    }
250
251    pub fn from_points(
252        start_point_marker: &PositionMarker,
253        end_point_marker: &PositionMarker,
254    ) -> PositionMarker {
255        Self {
256            data: PositionMarkerData {
257                source_slice: start_point_marker.source_slice.start
258                    ..end_point_marker.source_slice.end,
259                templated_slice: start_point_marker.templated_slice.start
260                    ..end_point_marker.templated_slice.end,
261                templated_file: start_point_marker.templated_file.clone(),
262                working_line_no: start_point_marker.working_line_no,
263                working_line_pos: start_point_marker.working_line_pos,
264            }
265            .into(),
266        }
267    }
268
269    pub(crate) fn with_working_position(
270        mut self,
271        line_no: usize,
272        line_pos: usize,
273    ) -> PositionMarker {
274        self.working_line_no = line_no;
275        self.working_line_pos = line_pos;
276        self
277    }
278
279    pub(crate) fn is_point(&self) -> bool {
280        self.source_slice.is_empty() && self.templated_slice.is_empty()
281    }
282}
283
284impl PartialEq for PositionMarker {
285    fn eq(&self, other: &Self) -> bool {
286        self.working_loc() == other.working_loc()
287    }
288}
289
290impl PartialOrd for PositionMarker {
291    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
292        Some(self.working_loc().cmp(&other.working_loc()))
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use std::ops::Range;
299
300    use crate::parser::markers::PositionMarker;
301    use crate::templaters::base::TemplatedFile;
302
303    /// Test that we can correctly infer positions from strings.
304    #[test]
305    fn test_markers_infer_next_position() {
306        struct Test {
307            raw: String,
308            start: Range<usize>,
309            end: Range<usize>,
310        }
311
312        let tests: Vec<Test> = vec![
313            Test {
314                raw: "fsaljk".to_string(),
315                start: 0..0,
316                end: 0..6,
317            },
318            Test {
319                raw: "".to_string(),
320                start: 2..2,
321                end: 2..2,
322            },
323            Test {
324                raw: "\n".to_string(),
325                start: 2..2,
326                end: 3..1,
327            },
328            Test {
329                raw: "boo\n".to_string(),
330                start: 2..2,
331                end: 3..1,
332            },
333            Test {
334                raw: "boo\nfoo".to_string(),
335                start: 2..2,
336                end: 3..4,
337            },
338            Test {
339                raw: "\nfoo".to_string(),
340                start: 2..2,
341                end: 3..4,
342            },
343        ];
344
345        for t in tests {
346            assert_eq!(
347                (t.end.start, t.end.end),
348                PositionMarker::infer_next_position(&t.raw, t.start.start, t.start.end)
349            );
350        }
351    }
352
353    /// Test that we can correctly infer positions from strings & locations.
354    #[test]
355    fn test_markers_setting_position_raw() {
356        let template: TemplatedFile = "foobar".into();
357        // Check inference in the template
358        assert_eq!(template.get_line_pos_of_char_pos(2, true), (1, 3));
359        assert_eq!(template.get_line_pos_of_char_pos(2, false), (1, 3));
360        // Now check it passes through
361        let pos = PositionMarker::new(2..5, 2..5, template, None, None);
362        // Can we infer positions correctly
363        assert_eq!(pos.working_loc(), (1, 3));
364    }
365
366    /// Test that we can correctly set positions manually.
367    #[test]
368    fn test_markers_setting_position_working() {
369        let templ: TemplatedFile = "foobar".into();
370        let pos = PositionMarker::new(2..5, 2..5, templ, Some(4), Some(4));
371        // Can we NOT infer when we're told.
372        assert_eq!(pos.working_loc(), (4, 4))
373    }
374
375    /// Test that we can correctly compare markers.
376    #[test]
377    fn test_markers_comparison() {
378        let templ: TemplatedFile = "abc".into();
379
380        // Assuming start and end are usize, based on typical Rust slicing/indexing.
381        let a_pos = PositionMarker::new(0..1, 0..1, templ.clone(), None, None);
382        let b_pos = PositionMarker::new(1..2, 1..2, templ.clone(), None, None);
383        let c_pos = PositionMarker::new(2..3, 2..3, templ.clone(), None, None);
384
385        let all_pos = [&a_pos, &b_pos, &c_pos];
386
387        // Check equality
388        assert!(all_pos.iter().all(|p| p == p));
389
390        // Check inequality
391        assert!(a_pos != b_pos && a_pos != c_pos && b_pos != c_pos);
392
393        // TODO Finish these tests
394        // Check less than
395        assert!(a_pos < b_pos && b_pos < c_pos);
396        assert!(!(c_pos < a_pos));
397
398        // Check greater than
399        assert!(c_pos > a_pos && c_pos > b_pos);
400        assert!(!(a_pos > c_pos));
401
402        // Check less than or equal
403        assert!(all_pos.iter().all(|p| a_pos <= **p));
404
405        // Check greater than or equal
406        assert!(all_pos.iter().all(|p| c_pos >= **p));
407    }
408}