Skip to main content

sqruff_lib_core/parser/
markers.rs

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