sqruff_lib/core/linter/
linted_file.rs

1use std::ops::Range;
2
3use crate::core::rules::noqa::IgnoreMask;
4use itertools::Itertools;
5use rustc_hash::FxHashSet;
6use sqruff_lib_core::errors::{SQLBaseError, SqlError};
7use sqruff_lib_core::parser::segments::fix::FixPatch;
8use sqruff_lib_core::templaters::base::{RawFileSlice, TemplatedFile};
9
10#[derive(Debug, Default, Clone)]
11pub struct LintedFile {
12    pub path: String,
13    pub patches: Vec<FixPatch>,
14    pub templated_file: TemplatedFile,
15    pub violations: Vec<SQLBaseError>,
16    pub ignore_mask: Option<IgnoreMask>,
17}
18
19impl LintedFile {
20    pub fn has_violations(&self) -> bool {
21        !self.violations.is_empty()
22    }
23
24    pub fn get_violations(&self, is_fixable: Option<bool>) -> Vec<SQLBaseError> {
25        if let Some(is_fixable) = is_fixable {
26            self.violations
27                .iter()
28                .filter(|v| v.fixable() == is_fixable)
29                .cloned()
30                .collect_vec()
31        } else {
32            self.violations.clone().into_iter().map_into().collect_vec()
33        }
34    }
35
36    ///  Use patches and raw file to fix the source file.
37    ///
38    ///  This assumes that patches and slices have already
39    ///  been coordinated. If they haven't then this will
40    ///  fail because we rely on patches having a corresponding
41    ///  slice of exactly the right file in the list of file
42    ///  slices.
43    pub fn build_up_fixed_source_string(
44        source_file_slices: &[Range<usize>],
45        source_patches: &[FixPatch],
46        raw_source_string: &str,
47    ) -> String {
48        // Iterate through the patches, building up the new string.
49        let mut str_buff = String::new();
50        for source_slice in source_file_slices.iter() {
51            // Is it one in the patch buffer:
52            let mut is_patched = false;
53            for patch in source_patches.iter() {
54                if patch.source_slice == *source_slice {
55                    str_buff.push_str(&patch.fixed_raw);
56                    is_patched = true;
57                    break;
58                }
59            }
60            if !is_patched {
61                // Use the raw string
62                str_buff.push_str(&raw_source_string[source_slice.start..source_slice.end]);
63            }
64        }
65        str_buff
66    }
67
68    pub fn fix_string(self) -> String {
69        // Generate patches from the fixed tree. In the process we sort
70        // and deduplicate them so that the resultant list is in the
71        //  right order for the source file without any duplicates.
72        let filtered_source_patches =
73            Self::generate_source_patches(self.patches, &self.templated_file);
74
75        // Any Template tags in the source file are off limits, unless we're explicitly
76        // fixing the source file.
77        let source_only_slices = self.templated_file.source_only_slices();
78
79        // We now slice up the file using the patches and any source only slices.
80        // This gives us regions to apply changes to.
81        let slice_buff = Self::slice_source_file_using_patches(
82            filtered_source_patches.clone(),
83            source_only_slices,
84            &self.templated_file.source_str,
85        );
86
87        Self::build_up_fixed_source_string(
88            &slice_buff,
89            &filtered_source_patches,
90            &self.templated_file.source_str,
91        )
92    }
93
94    fn generate_source_patches(
95        patches: Vec<FixPatch>,
96        _templated_file: &TemplatedFile,
97    ) -> Vec<FixPatch> {
98        let mut filtered_source_patches = Vec::new();
99        let mut dedupe_buffer = FxHashSet::default();
100
101        for patch in patches {
102            if dedupe_buffer.insert(patch.dedupe_tuple()) {
103                filtered_source_patches.push(patch);
104            }
105        }
106
107        filtered_source_patches.sort_by_key(|x| x.source_slice.start);
108        filtered_source_patches
109    }
110
111    ///  Use patches to safely slice up the file before fixing.
112    ///
113    ///  This uses source only slices to avoid overwriting sections
114    ///  of templated code in the source file (when we don't want to).
115    ///
116    ///  We assume that the source patches have already been
117    ///  sorted and deduplicated. Sorting is important. If the slices
118    ///  aren't sorted then this function will miss chunks.
119    ///  If there are overlaps or duplicates then this function
120    ///  may produce strange results.
121    fn slice_source_file_using_patches(
122        source_patches: Vec<FixPatch>,
123        mut source_only_slices: Vec<RawFileSlice>,
124        raw_source_string: &str,
125    ) -> Vec<Range<usize>> {
126        // We now slice up the file using the patches and any source only slices.
127        // This gives us regions to apply changes to.
128        let mut slice_buff: Vec<Range<usize>> = Vec::new();
129        let mut source_idx = 0;
130
131        for patch in &source_patches {
132            // Are there templated slices at or before the start of this patch?
133            // TODO: We'll need to explicit handling for template fixes here, because
134            // they ARE source only slices. If we can get handling to work properly
135            // here then this is the last hurdle and it will flow through
136            // smoothly from here.
137            while source_only_slices
138                .first()
139                .is_some_and(|s| s.source_idx < patch.source_slice.start)
140            {
141                let next_so_slice = source_only_slices.remove(0).source_slice();
142                // Add a pre-slice before the next templated slices if needed.
143                if next_so_slice.end > source_idx {
144                    slice_buff.push(source_idx..next_so_slice.start);
145                }
146                // Add the templated slice.
147                slice_buff.push(next_so_slice.clone());
148                source_idx = next_so_slice.end;
149            }
150
151            // Does this patch cover the next source-only slice directly?
152            if source_only_slices
153                .first()
154                .is_some_and(|s| patch.source_slice == s.source_slice())
155            {
156                // Log information here if needed
157                // Removing next source only slice from the stack because it
158                // covers the same area of source file as the current patch.
159                source_only_slices.remove(0);
160            }
161
162            // Is there a gap between current position and this patch?
163            if patch.source_slice.start > source_idx {
164                // Add a slice up to this patch.
165                slice_buff.push(source_idx..patch.source_slice.start);
166            }
167
168            // Is this patch covering an area we've already covered?
169            if patch.source_slice.start < source_idx {
170                // NOTE: This shouldn't happen. With more detailed templating
171                // this shouldn't happen - but in the off-chance that this does
172                // happen - then this code path remains.
173                // Log information here if needed
174                // Skipping overlapping patch at Index.
175                continue;
176            }
177
178            // Add this patch.
179            slice_buff.push(patch.source_slice.clone());
180            source_idx = patch.source_slice.end;
181        }
182        // Add a tail slice.
183        if source_idx < raw_source_string.len() {
184            slice_buff.push(source_idx..raw_source_string.len());
185        }
186
187        slice_buff
188    }
189}
190
191#[cfg(test)]
192mod test {
193    use sqruff_lib_core::templaters::base::TemplatedFileSlice;
194
195    use super::*;
196
197    /// Test _build_up_fixed_source_string. This is part of fix_string().
198    #[test]
199    fn test_linted_file_build_up_fixed_source_string() {
200        let tests = [
201            // Trivial example
202            (vec![0..1], vec![], "a", "a"),
203            // Simple replacement
204            (
205                vec![0..1, 1..2, 2..3],
206                vec![FixPatch::new(
207                    1..2,
208                    "d".into(),
209                    1..2,
210                    "b".into(),
211                    "b".into(),
212                )],
213                "abc",
214                "adc",
215            ),
216            // Simple insertion
217            (
218                vec![0..1, 1..1, 1..2],
219                vec![FixPatch::new(1..1, "b".into(), 1..1, "".into(), "".into())],
220                "ac",
221                "abc",
222            ),
223            // Simple deletion
224            (
225                vec![0..1, 1..2, 2..3],
226                vec![FixPatch::new(1..2, "".into(), 1..2, "b".into(), "b".into())],
227                "abc",
228                "ac",
229            ),
230            // Illustrative templated example (although practically at this step, the routine
231            // shouldn't care if it's templated).
232            (
233                vec![0..2, 2..7, 7..9],
234                vec![FixPatch::new(
235                    2..3,
236                    "{{ b }}".into(),
237                    2..7,
238                    "b".into(),
239                    "{{b}}".into(),
240                )],
241                "a {{b}} c",
242                "a {{ b }} c",
243            ),
244        ];
245
246        for (source_file_slices, source_patches, raw_source_string, expected_result) in tests {
247            let result = LintedFile::build_up_fixed_source_string(
248                &source_file_slices,
249                &source_patches,
250                raw_source_string,
251            );
252
253            assert_eq!(result, expected_result)
254        }
255    }
256
257    /// Test _slice_source_file_using_patches.
258    ///
259    ///     This is part of fix_string().
260    #[test]
261    fn test_slice_source_file_using_patches() {
262        let test_cases = [
263            (
264                // Trivial example.
265                // No edits in a single character file. Slice should be one
266                // character long.
267                vec![],
268                vec![],
269                "a",
270                vec![0..1],
271            ),
272            (
273                // Simple replacement.
274                // We've yielded a patch to change a single character. This means
275                // we should get only slices for that character, and for the
276                // unchanged file around it.
277                vec![FixPatch::new(
278                    1..2,
279                    "d".into(),
280                    1..2,
281                    "b".into(),
282                    "b".into(),
283                )],
284                vec![],
285                "abc",
286                vec![0..1, 1..2, 2..3],
287            ),
288            (
289                // Templated no fixes.
290                // A templated file, but with no fixes, so no subdivision of the
291                // file is required, and we should just get a single slice.
292                vec![],
293                vec![],
294                "a {{ b }} c",
295                vec![0..11],
296            ),
297            (
298                // Templated example with a source-only slice.
299                // A templated file, but with no fixes, so no subdivision of the
300                // file is required and we should just get a single slice. While
301                // there is handling for "source only" slices like template
302                // comments, in this case no additional slicing is required
303                // because no edits have been made.
304                vec![],
305                vec![RawFileSlice::new(
306                    "{# b #}".into(),
307                    "comment".into(),
308                    2,
309                    None,
310                    None,
311                )],
312                "a {# b #} c",
313                vec![0..11],
314            ),
315            (
316                // Templated fix example with a source-only slice.
317                // We're making an edit adjacent to a source only slice. Edits
318                // _before_ source only slices currently don't trigger additional
319                // slicing. This is fine.
320                vec![FixPatch::new(
321                    0..1,
322                    "a ".into(),
323                    0..1,
324                    "a".into(),
325                    "a".into(),
326                )],
327                vec![RawFileSlice::new(
328                    "{# b #}".into(),
329                    "comment".into(),
330                    1,
331                    None,
332                    None,
333                )],
334                "a{# b #}c",
335                vec![0..1, 1..9],
336            ),
337            (
338                // Templated fix example with a source-only slice.
339                // We've made an edit directly _after_ a source only slice
340                // which should trigger the logic to ensure that the source
341                // only slice isn't included in the source mapping of the
342                // edit.
343                vec![FixPatch::new(
344                    1..2,
345                    " c".into(),
346                    8..9,
347                    "c".into(),
348                    "c".into(),
349                )],
350                vec![RawFileSlice::new(
351                    "{# b #}".into(),
352                    "comment".into(),
353                    1,
354                    None,
355                    None,
356                )],
357                "a{# b #}cc",
358                vec![0..1, 1..8, 8..9, 9..10],
359            ),
360            (
361                // Templated example with a source-only slice.
362                // Here we're making the fix to the templated slice. This
363                // checks that we don't duplicate or fumble the slice
364                // generation when we're explicitly trying to edit the source.
365                vec![FixPatch::new(
366                    2..2,
367                    "{# fixed #}".into(),
368                    // "".into(),
369                    2..9,
370                    "".into(),
371                    "".into(),
372                )],
373                vec![RawFileSlice::new(
374                    "{# b #}".into(),
375                    "comment".into(),
376                    2,
377                    None,
378                    None,
379                )],
380                "a {# b #} c",
381                vec![0..2, 2..9, 9..11],
382            ),
383            (
384                // Illustrate potential templating bug (case from JJ01).
385                // In this case we have fixes for all our tempolated sections
386                // and they are all close to each other and so may be either
387                // skipped or duplicated if the logic is not precise.
388                vec![
389                    FixPatch::new(
390                        14..14,
391                        "{%+ if true -%}".into(),
392                        // "source".into(),
393                        14..27,
394                        "".into(),
395                        "{%+if true-%}".into(),
396                    ),
397                    FixPatch::new(
398                        14..14,
399                        "{{ ref('foo') }}".into(),
400                        // "source".into(),
401                        28..42,
402                        "".into(),
403                        "{{ref('foo')}}".into(),
404                    ),
405                    FixPatch::new(
406                        17..17,
407                        "{%- endif %}".into(),
408                        // "source".into(),
409                        43..53,
410                        "".into(),
411                        "{%-endif%}".into(),
412                    ),
413                ],
414                vec![
415                    RawFileSlice::new("{%+if true-%}".into(), "block_start".into(), 14, None, None),
416                    RawFileSlice::new("{%-endif%}".into(), "block_end".into(), 43, None, None),
417                ],
418                "SELECT 1 from {%+if true-%} {{ref('foo')}} {%-endif%}",
419                vec![0..14, 14..27, 27..28, 28..42, 42..43, 43..53],
420            ),
421        ];
422
423        for (source_patches, source_only_slices, raw_source_string, expected_result) in test_cases {
424            let result = LintedFile::slice_source_file_using_patches(
425                source_patches,
426                source_only_slices,
427                raw_source_string,
428            );
429            assert_eq!(result, expected_result);
430        }
431    }
432
433    #[allow(dead_code)]
434    fn templated_file_1() -> TemplatedFile {
435        "abc".into()
436    }
437
438    #[allow(dead_code)]
439    fn templated_file_2() -> TemplatedFile {
440        TemplatedFile::new(
441            "{# blah #}{{ foo }}bc".into(),
442            "<testing>".into(),
443            Some("abc".into()),
444            Some(vec![
445                TemplatedFileSlice::new("comment", 0..10, 0..0),
446                TemplatedFileSlice::new("templated", 10..19, 0..1),
447                TemplatedFileSlice::new("literal", 19..21, 1..3),
448            ]),
449            Some(vec![
450                RawFileSlice::new("{# blah #}".into(), "comment".into(), 0, None, None),
451                RawFileSlice::new("{{ foo }}".into(), "templated".into(), 10, None, None),
452                RawFileSlice::new("bc".into(), "literal".into(), 19, None, None),
453            ]),
454        )
455        .unwrap()
456    }
457}