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::Replace { anchor, edit, .. } = self
214            && edit.len() == 1
215        {
216            let edit_seg = &edit[0];
217            if edit_seg.raw() == anchor.raw() && !edit_seg.get_source_fixes().is_empty() {
218                return false;
219            }
220        }
221
222        let check_fn = match self {
223            LintFix::CreateAfter { .. } | LintFix::CreateBefore { .. } => itertools::all,
224            _ => itertools::any,
225        };
226
227        let fix_slices = self.fix_slices(templated_file, false);
228        let result = check_fn(fix_slices, |fs: RawFileSlice| {
229            fs.has_slice_kind(TemplateSliceKind::Templated)
230        });
231
232        let source_is_empty = match self {
233            LintFix::CreateAfter { source, .. } | LintFix::Replace { source, .. } => {
234                source.is_empty()
235            }
236            _ => true,
237        };
238
239        if result || source_is_empty {
240            return result;
241        }
242
243        let templated_slices = None;
244        let raw_slices = self.raw_slices_from_templated_slices(
245            templated_file,
246            templated_slices.into_iter(),
247            None,
248        );
249        raw_slices
250            .iter()
251            .any(|fs| fs.has_slice_kind(TemplateSliceKind::Templated))
252    }
253}
254
255impl PartialEq for LintFix {
256    fn eq(&self, other: &Self) -> bool {
257        use std::mem::discriminant;
258
259        // Check if variants are the same
260        if discriminant(self) != discriminant(other) {
261            return false;
262        }
263
264        match (self, other) {
265            (
266                LintFix::CreateBefore {
267                    anchor: a1,
268                    edit: e1,
269                },
270                LintFix::CreateBefore {
271                    anchor: a2,
272                    edit: e2,
273                },
274            ) => {
275                a1.get_type() == a2.get_type()
276                    && a1.id() == a2.id()
277                    && e1.len() == e2.len()
278                    && e1.iter().zip(e2).all(|(s1, s2)| {
279                        s1.raw() == s2.raw() && s1.get_source_fixes() == s2.get_source_fixes()
280                    })
281            }
282            (
283                LintFix::CreateAfter {
284                    anchor: a1,
285                    edit: e1,
286                    source: s1,
287                },
288                LintFix::CreateAfter {
289                    anchor: a2,
290                    edit: e2,
291                    source: s2,
292                },
293            ) => {
294                a1.get_type() == a2.get_type()
295                    && a1.id() == a2.id()
296                    && e1.len() == e2.len()
297                    && e1.iter().zip(e2).all(|(seg1, seg2)| {
298                        seg1.raw() == seg2.raw()
299                            && seg1.get_source_fixes() == seg2.get_source_fixes()
300                    })
301                    && s1 == s2
302            }
303            (
304                LintFix::Replace {
305                    anchor: a1,
306                    edit: e1,
307                    source: s1,
308                },
309                LintFix::Replace {
310                    anchor: a2,
311                    edit: e2,
312                    source: s2,
313                },
314            ) => {
315                a1.get_type() == a2.get_type()
316                    && a1.id() == a2.id()
317                    && e1.len() == e2.len()
318                    && e1.iter().zip(e2).all(|(seg1, seg2)| {
319                        seg1.raw() == seg2.raw()
320                            && seg1.get_source_fixes() == seg2.get_source_fixes()
321                    })
322                    && s1 == s2
323            }
324            (LintFix::Delete { anchor: a1 }, LintFix::Delete { anchor: a2 }) => {
325                a1.get_type() == a2.get_type() && a1.id() == a2.id()
326            }
327            _ => false,
328        }
329    }
330}