sqruff_lib_core/
lint_fix.rs

1use std::ops::Range;
2
3use ahash::AHashSet;
4
5use crate::edit_type::EditType;
6use crate::parser::segments::base::ErasedSegment;
7use crate::templaters::base::{RawFileSlice, TemplatedFile};
8
9/// A potential fix to a linting violation.
10#[derive(Debug, Clone)]
11pub struct LintFix {
12    /// indicate the kind of fix this represents
13    pub edit_type: EditType,
14    /// A segment which represents the *position* that this fix should be applied at. For
15    /// - deletions, it represents the segment to delete
16    /// - creations, it implies the position to create at (with the existing element at this position to be moved *after* the edit),
17    /// - `replace`, it  implies the segment to be replaced.
18    pub anchor: ErasedSegment,
19    /// For `replace` and `create` fixes, this holds the iterable of segments to create or replace
20    /// at the given `anchor` point.
21    pub edit: Vec<ErasedSegment>,
22    /// For `replace` and `create` fixes, this holds iterable of segments that provided
23    /// code. IMPORTANT: The linter uses this to prevent copying material
24    /// from templated areas.
25    pub source: Vec<ErasedSegment>,
26}
27
28impl LintFix {
29    fn new(
30        edit_type: EditType,
31        anchor: ErasedSegment,
32        mut edit: Vec<ErasedSegment>,
33        source: Option<Vec<ErasedSegment>>,
34    ) -> Self {
35        // If `edit` is provided, copy all elements and strip position markers.
36        // Developer Note: Ensure position markers are unset for all edit segments.
37        // We rely on realignment to make position markers later in the process.
38        for seg in &mut edit {
39            if seg.get_position_marker().is_some() {
40                seg.make_mut().set_position_marker(None);
41            };
42        }
43
44        // If `source` is provided, filter segments with position markers.
45        let clean_source = source.map_or(Vec::new(), |source| {
46            source
47                .into_iter()
48                .filter(|seg| seg.get_position_marker().is_some())
49                .collect()
50        });
51
52        LintFix {
53            edit_type,
54            anchor,
55            edit,
56            source: clean_source,
57        }
58    }
59
60    pub fn create_before(anchor: ErasedSegment, edit_segments: Vec<ErasedSegment>) -> Self {
61        Self::new(EditType::CreateBefore, anchor, edit_segments, None)
62    }
63
64    pub fn create_after(
65        anchor: ErasedSegment,
66        edit_segments: Vec<ErasedSegment>,
67        source: Option<Vec<ErasedSegment>>,
68    ) -> Self {
69        Self::new(EditType::CreateAfter, anchor, edit_segments, source)
70    }
71
72    pub fn replace(
73        anchor_segment: ErasedSegment,
74        edit_segments: Vec<ErasedSegment>,
75        source: Option<Vec<ErasedSegment>>,
76    ) -> Self {
77        Self::new(EditType::Replace, anchor_segment, edit_segments, source)
78    }
79
80    pub fn delete(anchor_segment: ErasedSegment) -> Self {
81        Self::new(EditType::Delete, anchor_segment, Vec::new(), None)
82    }
83
84    /// Return whether this a valid source only edit.
85    pub fn is_just_source_edit(&self) -> bool {
86        self.edit_type == EditType::Replace
87            && self.edit.len() == 1
88            && self.edit[0].raw() == self.anchor.raw()
89    }
90
91    fn fix_slices(
92        &self,
93        templated_file: &TemplatedFile,
94        within_only: bool,
95    ) -> AHashSet<RawFileSlice> {
96        let anchor_slice = self
97            .anchor
98            .get_position_marker()
99            .unwrap()
100            .templated_slice
101            .clone();
102
103        let adjust_boundary = if !within_only { 1 } else { 0 };
104
105        let templated_slice = match self.edit_type {
106            EditType::CreateBefore => {
107                anchor_slice.start.saturating_sub(1)..anchor_slice.start + adjust_boundary
108            }
109            EditType::CreateAfter => anchor_slice.end - adjust_boundary..anchor_slice.end + 1,
110            EditType::Replace => {
111                let pos = self.anchor.get_position_marker().unwrap();
112                if pos.source_slice.start == pos.source_slice.end {
113                    return AHashSet::new();
114                } else if self
115                    .edit
116                    .iter()
117                    .all(|it| it.segments().is_empty() && !it.get_source_fixes().is_empty())
118                {
119                    let source_edit_slices: Vec<_> = self
120                        .edit
121                        .iter()
122                        .flat_map(|edit| edit.get_source_fixes())
123                        .map(|source_fixe| source_fixe.source_slice.clone())
124                        .collect();
125
126                    let slice =
127                        templated_file.raw_slices_spanning_source_slice(&source_edit_slices[0]);
128                    return AHashSet::from_iter(slice);
129                }
130
131                anchor_slice
132            }
133            _ => anchor_slice,
134        };
135
136        self.raw_slices_from_templated_slices(
137            templated_file,
138            std::iter::once(templated_slice),
139            RawFileSlice::new(String::new(), "literal".to_string(), usize::MAX, None, None).into(),
140        )
141    }
142
143    fn raw_slices_from_templated_slices(
144        &self,
145        templated_file: &TemplatedFile,
146        templated_slices: impl Iterator<Item = Range<usize>>,
147        file_end_slice: Option<RawFileSlice>,
148    ) -> AHashSet<RawFileSlice> {
149        let mut raw_slices = AHashSet::new();
150
151        for templated_slice in templated_slices {
152            let templated_slice =
153                templated_file.templated_slice_to_source_slice(templated_slice.clone());
154
155            match templated_slice {
156                Ok(templated_slice) => raw_slices
157                    .extend(templated_file.raw_slices_spanning_source_slice(&templated_slice)),
158                Err(_) => {
159                    if let Some(file_end_slice) = file_end_slice.clone() {
160                        raw_slices.insert(file_end_slice);
161                    }
162                }
163            }
164        }
165
166        raw_slices
167    }
168
169    pub fn has_template_conflicts(&self, templated_file: &TemplatedFile) -> bool {
170        if self.edit_type == EditType::Replace && self.edit.len() == 1 {
171            let edit = &self.edit[0];
172            if edit.raw() == self.anchor.raw() && !edit.get_source_fixes().is_empty() {
173                return false;
174            }
175        }
176
177        let check_fn = if let EditType::CreateAfter | EditType::CreateBefore = self.edit_type {
178            itertools::all
179        } else {
180            itertools::any
181        };
182
183        let fix_slices = self.fix_slices(templated_file, false);
184        let result = check_fn(fix_slices, |fs: RawFileSlice| fs.slice_type == "templated");
185
186        if result || self.source.is_empty() {
187            return result;
188        }
189
190        let templated_slices = None;
191        let raw_slices = self.raw_slices_from_templated_slices(
192            templated_file,
193            templated_slices.into_iter(),
194            None,
195        );
196        raw_slices.iter().any(|fs| fs.slice_type == "templated")
197    }
198}
199
200impl PartialEq for LintFix {
201    fn eq(&self, other: &Self) -> bool {
202        // Check if edit_types are equal
203        if self.edit_type != other.edit_type {
204            return false;
205        }
206        // Check if anchor.class_types are equal
207        if self.anchor.get_type() != other.anchor.get_type() {
208            return false;
209        }
210        // Check if anchor.uuids are equal
211        if self.anchor.id() != other.anchor.id() {
212            return false;
213        }
214
215        // Check lengths
216        if self.edit.len() != other.edit.len() {
217            return false;
218        }
219        // Compare raw and source_fixes for each corresponding BaseSegment
220        for (self_base_segment, other_base_segment) in self.edit.iter().zip(&other.edit) {
221            if self_base_segment.raw() != other_base_segment.raw()
222                || self_base_segment.get_source_fixes() != other_base_segment.get_source_fixes()
223            {
224                return false;
225            }
226        }
227        // If none of the above conditions were met, objects are equal
228        true
229    }
230}