sqruff_lib_core/
lint_fix.rs

1use std::ops::Range;
2
3use ahash::AHashSet;
4
5use crate::parser::segments::ErasedSegment;
6use crate::templaters::{RawFileSlice, TemplatedFile};
7
8/// A potential fix to a linting violation.
9#[derive(Debug, Clone)]
10pub enum LintFix {
11    /// Create a fix before an anchor segment
12    CreateBefore {
13        /// The segment before which to create the edit
14        anchor: ErasedSegment,
15        /// The segments to create
16        edit: Vec<ErasedSegment>,
17    },
18    /// Create a fix after an anchor segment
19    CreateAfter {
20        /// The segment after which to create the edit
21        anchor: ErasedSegment,
22        /// The segments to create
23        edit: Vec<ErasedSegment>,
24        /// Segments that provided the source code
25        source: Vec<ErasedSegment>,
26    },
27    /// Replace an anchor segment with new segments
28    Replace {
29        /// The segment to replace
30        anchor: ErasedSegment,
31        /// The segments to replace with
32        edit: Vec<ErasedSegment>,
33        /// Segments that provided the source code
34        source: Vec<ErasedSegment>,
35    },
36    /// Delete an anchor segment
37    Delete {
38        /// The segment to delete
39        anchor: ErasedSegment,
40    },
41}
42
43impl LintFix {
44    /// Get the anchor segment for this fix
45    pub fn anchor(&self) -> &ErasedSegment {
46        match self {
47            LintFix::CreateBefore { anchor, .. } => anchor,
48            LintFix::CreateAfter { anchor, .. } => anchor,
49            LintFix::Replace { anchor, .. } => anchor,
50            LintFix::Delete { anchor } => anchor,
51        }
52    }
53
54    fn prepare_edit_segments(mut edit: Vec<ErasedSegment>) -> Vec<ErasedSegment> {
55        // Copy all elements and strip position markers.
56        // Developer Note: Ensure position markers are unset for all edit segments.
57        // We rely on realignment to make position markers later in the process.
58        for seg in &mut edit {
59            if seg.get_position_marker().is_some() {
60                seg.make_mut().set_position_marker(None);
61            };
62        }
63        edit
64    }
65
66    fn prepare_source_segments(source: Option<Vec<ErasedSegment>>) -> Vec<ErasedSegment> {
67        // If `source` is provided, filter segments with position markers.
68        source.map_or(Vec::new(), |source| {
69            source
70                .into_iter()
71                .filter(|seg| seg.get_position_marker().is_some())
72                .collect()
73        })
74    }
75
76    pub fn create_before(anchor: ErasedSegment, edit_segments: Vec<ErasedSegment>) -> Self {
77        LintFix::CreateBefore {
78            anchor,
79            edit: Self::prepare_edit_segments(edit_segments),
80        }
81    }
82
83    pub fn create_after(
84        anchor: ErasedSegment,
85        edit_segments: Vec<ErasedSegment>,
86        source: Option<Vec<ErasedSegment>>,
87    ) -> Self {
88        LintFix::CreateAfter {
89            anchor,
90            edit: Self::prepare_edit_segments(edit_segments),
91            source: Self::prepare_source_segments(source),
92        }
93    }
94
95    pub fn replace(
96        anchor_segment: ErasedSegment,
97        edit_segments: Vec<ErasedSegment>,
98        source: Option<Vec<ErasedSegment>>,
99    ) -> Self {
100        LintFix::Replace {
101            anchor: anchor_segment,
102            edit: Self::prepare_edit_segments(edit_segments),
103            source: Self::prepare_source_segments(source),
104        }
105    }
106
107    pub fn delete(anchor_segment: ErasedSegment) -> Self {
108        LintFix::Delete {
109            anchor: anchor_segment,
110        }
111    }
112
113    /// Return whether this a valid source only edit.
114    pub fn is_just_source_edit(&self) -> bool {
115        match self {
116            LintFix::Replace { anchor, edit, .. } => {
117                edit.len() == 1 && edit[0].raw() == anchor.raw()
118            }
119            _ => false,
120        }
121    }
122
123    fn fix_slices(
124        &self,
125        templated_file: &TemplatedFile,
126        within_only: bool,
127    ) -> AHashSet<RawFileSlice> {
128        let anchor = match self {
129            LintFix::CreateBefore { anchor, .. } => anchor,
130            LintFix::CreateAfter { anchor, .. } => anchor,
131            LintFix::Replace { anchor, .. } => anchor,
132            LintFix::Delete { anchor } => anchor,
133        };
134
135        let anchor_slice = anchor
136            .get_position_marker()
137            .unwrap()
138            .templated_slice
139            .clone();
140
141        let adjust_boundary = if !within_only { 1 } else { 0 };
142
143        let templated_slice = match self {
144            LintFix::CreateBefore { .. } => {
145                anchor_slice.start.saturating_sub(1)..anchor_slice.start + adjust_boundary
146            }
147            LintFix::CreateAfter { .. } => anchor_slice.end - adjust_boundary..anchor_slice.end + 1,
148            LintFix::Replace { anchor, edit, .. } => {
149                let pos = anchor.get_position_marker().unwrap();
150                if pos.source_slice.start == pos.source_slice.end {
151                    return AHashSet::new();
152                } else if edit
153                    .iter()
154                    .all(|it| it.segments().is_empty() && !it.get_source_fixes().is_empty())
155                {
156                    let source_edit_slices: Vec<_> = edit
157                        .iter()
158                        .flat_map(|edit| edit.get_source_fixes())
159                        .map(|source_fixe| source_fixe.source_slice.clone())
160                        .collect();
161
162                    let slice =
163                        templated_file.raw_slices_spanning_source_slice(&source_edit_slices[0]);
164                    return AHashSet::from_iter(slice);
165                }
166
167                anchor_slice
168            }
169            LintFix::Delete { .. } => anchor_slice,
170        };
171
172        self.raw_slices_from_templated_slices(
173            templated_file,
174            std::iter::once(templated_slice),
175            RawFileSlice::new(String::new(), "literal".to_string(), usize::MAX, None, None).into(),
176        )
177    }
178
179    fn raw_slices_from_templated_slices(
180        &self,
181        templated_file: &TemplatedFile,
182        templated_slices: impl Iterator<Item = Range<usize>>,
183        file_end_slice: Option<RawFileSlice>,
184    ) -> AHashSet<RawFileSlice> {
185        let mut raw_slices = AHashSet::new();
186
187        for templated_slice in templated_slices {
188            let templated_slice =
189                templated_file.templated_slice_to_source_slice(templated_slice.clone());
190
191            match templated_slice {
192                Ok(templated_slice) => raw_slices
193                    .extend(templated_file.raw_slices_spanning_source_slice(&templated_slice)),
194                Err(_) => {
195                    if let Some(file_end_slice) = file_end_slice.clone() {
196                        raw_slices.insert(file_end_slice);
197                    }
198                }
199            }
200        }
201
202        raw_slices
203    }
204
205    pub fn has_template_conflicts(&self, templated_file: &TemplatedFile) -> bool {
206        if let LintFix::Replace { anchor, edit, .. } = self
207            && edit.len() == 1
208        {
209            let edit_seg = &edit[0];
210            if edit_seg.raw() == anchor.raw() && !edit_seg.get_source_fixes().is_empty() {
211                return false;
212            }
213        }
214
215        let check_fn = match self {
216            LintFix::CreateAfter { .. } | LintFix::CreateBefore { .. } => itertools::all,
217            _ => itertools::any,
218        };
219
220        let fix_slices = self.fix_slices(templated_file, false);
221        let result = check_fn(fix_slices, |fs: RawFileSlice| fs.slice_type == "templated");
222
223        let source_is_empty = match self {
224            LintFix::CreateAfter { source, .. } | LintFix::Replace { source, .. } => {
225                source.is_empty()
226            }
227            _ => true,
228        };
229
230        if result || source_is_empty {
231            return result;
232        }
233
234        let templated_slices = None;
235        let raw_slices = self.raw_slices_from_templated_slices(
236            templated_file,
237            templated_slices.into_iter(),
238            None,
239        );
240        raw_slices.iter().any(|fs| fs.slice_type == "templated")
241    }
242}
243
244impl PartialEq for LintFix {
245    fn eq(&self, other: &Self) -> bool {
246        use std::mem::discriminant;
247
248        // Check if variants are the same
249        if discriminant(self) != discriminant(other) {
250            return false;
251        }
252
253        match (self, other) {
254            (
255                LintFix::CreateBefore {
256                    anchor: a1,
257                    edit: e1,
258                },
259                LintFix::CreateBefore {
260                    anchor: a2,
261                    edit: e2,
262                },
263            ) => {
264                a1.get_type() == a2.get_type()
265                    && a1.id() == a2.id()
266                    && e1.len() == e2.len()
267                    && e1.iter().zip(e2).all(|(s1, s2)| {
268                        s1.raw() == s2.raw() && s1.get_source_fixes() == s2.get_source_fixes()
269                    })
270            }
271            (
272                LintFix::CreateAfter {
273                    anchor: a1,
274                    edit: e1,
275                    source: s1,
276                },
277                LintFix::CreateAfter {
278                    anchor: a2,
279                    edit: e2,
280                    source: s2,
281                },
282            ) => {
283                a1.get_type() == a2.get_type()
284                    && a1.id() == a2.id()
285                    && e1.len() == e2.len()
286                    && e1.iter().zip(e2).all(|(seg1, seg2)| {
287                        seg1.raw() == seg2.raw()
288                            && seg1.get_source_fixes() == seg2.get_source_fixes()
289                    })
290                    && s1 == s2
291            }
292            (
293                LintFix::Replace {
294                    anchor: a1,
295                    edit: e1,
296                    source: s1,
297                },
298                LintFix::Replace {
299                    anchor: a2,
300                    edit: e2,
301                    source: s2,
302                },
303            ) => {
304                a1.get_type() == a2.get_type()
305                    && a1.id() == a2.id()
306                    && e1.len() == e2.len()
307                    && e1.iter().zip(e2).all(|(seg1, seg2)| {
308                        seg1.raw() == seg2.raw()
309                            && seg1.get_source_fixes() == seg2.get_source_fixes()
310                    })
311                    && s1 == s2
312            }
313            (LintFix::Delete { anchor: a1 }, LintFix::Delete { anchor: a2 }) => {
314                a1.get_type() == a2.get_type() && a1.id() == a2.id()
315            }
316            _ => false,
317        }
318    }
319}