Skip to main content

sqruff_lib_core/
lint_fix.rs

1use std::ops::Range;
2
3use hashbrown::HashSet;
4
5use crate::parser::segments::ErasedSegment;
6use crate::templaters::{RawFileSlice, TemplateSliceKind, 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    ) -> HashSet<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 HashSet::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 HashSet::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(
176                String::new(),
177                TemplateSliceKind::Literal,
178                usize::MAX,
179                None,
180                None,
181            )
182            .into(),
183        )
184    }
185
186    fn raw_slices_from_templated_slices(
187        &self,
188        templated_file: &TemplatedFile,
189        templated_slices: impl Iterator<Item = Range<usize>>,
190        file_end_slice: Option<RawFileSlice>,
191    ) -> HashSet<RawFileSlice> {
192        let mut raw_slices = HashSet::new();
193
194        for templated_slice in templated_slices {
195            let templated_slice =
196                templated_file.templated_slice_to_source_slice(templated_slice.clone());
197
198            match templated_slice {
199                Ok(templated_slice) => raw_slices
200                    .extend(templated_file.raw_slices_spanning_source_slice(&templated_slice)),
201                Err(_) => {
202                    if let Some(file_end_slice) = file_end_slice.clone() {
203                        raw_slices.insert(file_end_slice);
204                    }
205                }
206            }
207        }
208
209        raw_slices
210    }
211
212    pub fn has_template_conflicts(&self, templated_file: &TemplatedFile) -> bool {
213        if let LintFix::CreateAfter { anchor, .. } = self
214            && anchor
215                .get_position_marker()
216                .is_some_and(|pos| pos.source_slice.end == templated_file.source_str.len())
217        {
218            return false;
219        }
220
221        if let LintFix::Replace { anchor, edit, .. } = self
222            && edit.len() == 1
223        {
224            let edit_seg = &edit[0];
225            if edit_seg.raw() == anchor.raw() && !edit_seg.get_source_fixes().is_empty() {
226                return false;
227            }
228        }
229
230        let check_fn = match self {
231            LintFix::CreateAfter { .. } | LintFix::CreateBefore { .. } => itertools::all,
232            _ => itertools::any,
233        };
234
235        let fix_slices = self.fix_slices(templated_file, false);
236        let result = check_fn(fix_slices, |fs: RawFileSlice| {
237            !fs.has_slice_kind(TemplateSliceKind::Literal)
238        });
239
240        let source_is_empty = match self {
241            LintFix::CreateAfter { source, .. } | LintFix::Replace { source, .. } => {
242                source.is_empty()
243            }
244            _ => true,
245        };
246
247        if result || source_is_empty {
248            return result;
249        }
250
251        let templated_slices = None;
252        let raw_slices = self.raw_slices_from_templated_slices(
253            templated_file,
254            templated_slices.into_iter(),
255            None,
256        );
257        raw_slices
258            .iter()
259            .any(|fs| !fs.has_slice_kind(TemplateSliceKind::Literal))
260    }
261}
262
263impl PartialEq for LintFix {
264    fn eq(&self, other: &Self) -> bool {
265        use std::mem::discriminant;
266
267        // Check if variants are the same
268        if discriminant(self) != discriminant(other) {
269            return false;
270        }
271
272        match (self, other) {
273            (
274                LintFix::CreateBefore {
275                    anchor: a1,
276                    edit: e1,
277                },
278                LintFix::CreateBefore {
279                    anchor: a2,
280                    edit: e2,
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(|(s1, s2)| {
287                        s1.raw() == s2.raw() && s1.get_source_fixes() == s2.get_source_fixes()
288                    })
289            }
290            (
291                LintFix::CreateAfter {
292                    anchor: a1,
293                    edit: e1,
294                    source: s1,
295                },
296                LintFix::CreateAfter {
297                    anchor: a2,
298                    edit: e2,
299                    source: s2,
300                },
301            ) => {
302                a1.get_type() == a2.get_type()
303                    && a1.id() == a2.id()
304                    && e1.len() == e2.len()
305                    && e1.iter().zip(e2).all(|(seg1, seg2)| {
306                        seg1.raw() == seg2.raw()
307                            && seg1.get_source_fixes() == seg2.get_source_fixes()
308                    })
309                    && s1 == s2
310            }
311            (
312                LintFix::Replace {
313                    anchor: a1,
314                    edit: e1,
315                    source: s1,
316                },
317                LintFix::Replace {
318                    anchor: a2,
319                    edit: e2,
320                    source: s2,
321                },
322            ) => {
323                a1.get_type() == a2.get_type()
324                    && a1.id() == a2.id()
325                    && e1.len() == e2.len()
326                    && e1.iter().zip(e2).all(|(seg1, seg2)| {
327                        seg1.raw() == seg2.raw()
328                            && seg1.get_source_fixes() == seg2.get_source_fixes()
329                    })
330                    && s1 == s2
331            }
332            (LintFix::Delete { anchor: a1 }, LintFix::Delete { anchor: a2 }) => {
333                a1.get_type() == a2.get_type() && a1.id() == a2.id()
334            }
335            _ => false,
336        }
337    }
338}