sqruff_lib_core/
templaters.rs

1use std::cmp::Ordering;
2use std::ops::{Deref, Range};
3use std::sync::Arc;
4
5#[cfg(feature = "stringify")]
6use serde::{Deserialize, Serialize};
7
8use smol_str::SmolStr;
9
10use crate::errors::SQLFluffSkipFile;
11use crate::slice_helpers::zero_slice;
12
13/// A slice referring to a templated file.
14#[cfg_attr(feature = "stringify", derive(Serialize))]
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct TemplatedFileSlice {
17    pub slice_type: String,
18    pub source_slice: Range<usize>,
19    pub templated_slice: Range<usize>,
20}
21
22impl TemplatedFileSlice {
23    pub fn new(
24        slice_type: &str,
25        source_slice: Range<usize>,
26        templated_slice: Range<usize>,
27    ) -> Self {
28        Self {
29            slice_type: slice_type.to_string(),
30            source_slice,
31            templated_slice,
32        }
33    }
34}
35
36/// A templated SQL file.
37///
38/// This is the response of a `templater`'s `.process()` method
39/// and contains both references to the original file and also
40/// the capability to split up that file when lexing.
41#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)]
42pub struct TemplatedFile {
43    inner: Arc<TemplatedFileInner>,
44}
45
46impl TemplatedFile {
47    pub fn new(
48        source_str: String,
49        name: String,
50        input_templated_str: Option<String>,
51        sliced_file: Option<Vec<TemplatedFileSlice>>,
52        input_raw_sliced: Option<Vec<RawFileSlice>>,
53    ) -> Result<TemplatedFile, SQLFluffSkipFile> {
54        Ok(TemplatedFile {
55            inner: Arc::new(TemplatedFileInner::new(
56                source_str,
57                name,
58                input_templated_str,
59                sliced_file,
60                input_raw_sliced,
61            )?),
62        })
63    }
64
65    pub fn name(&self) -> &str {
66        &self.inner.name
67    }
68
69    #[cfg(feature = "stringify")]
70    pub fn to_yaml(&self) -> String {
71        let inner = &*self.inner;
72        serde_yaml::to_string(inner).unwrap()
73    }
74}
75
76impl From<String> for TemplatedFile {
77    fn from(raw: String) -> Self {
78        TemplatedFile {
79            inner: Arc::new(
80                TemplatedFileInner::new(raw, "<string>".to_string(), None, None, None).unwrap(),
81            ),
82        }
83    }
84}
85
86impl From<&str> for TemplatedFile {
87    fn from(raw: &str) -> Self {
88        TemplatedFile {
89            inner: Arc::new(
90                TemplatedFileInner::new(raw.to_string(), "<string>".to_string(), None, None, None)
91                    .unwrap(),
92            ),
93        }
94    }
95}
96
97impl Deref for TemplatedFile {
98    type Target = TemplatedFileInner;
99
100    fn deref(&self) -> &Self::Target {
101        &self.inner
102    }
103}
104
105#[cfg_attr(feature = "stringify", derive(Serialize))]
106#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)]
107pub struct TemplatedFileInner {
108    pub source_str: String,
109    name: String,
110    pub templated_str: Option<String>,
111    source_newlines: Vec<usize>,
112    templated_newlines: Vec<usize>,
113    raw_sliced: Vec<RawFileSlice>,
114    pub sliced_file: Vec<TemplatedFileSlice>,
115}
116
117impl TemplatedFileInner {
118    /// Initialise the TemplatedFile.
119    /// If no templated_str is provided then we assume that
120    /// the file is NOT templated and that the templated view
121    /// is the same as the source view.
122    pub fn new(
123        source_str: String,
124        f_name: String,
125        input_templated_str: Option<String>,
126        sliced_file: Option<Vec<TemplatedFileSlice>>,
127        input_raw_sliced: Option<Vec<RawFileSlice>>,
128    ) -> Result<TemplatedFileInner, SQLFluffSkipFile> {
129        // Assume that no sliced_file, means the file is not templated.
130        // TODO Will this not always be Some and so type can avoid Option?
131        let templated_str = input_templated_str.clone().unwrap_or(source_str.clone());
132
133        let (sliced_file, raw_sliced): (Vec<TemplatedFileSlice>, Vec<RawFileSlice>) =
134            match sliced_file {
135                None => {
136                    if templated_str != source_str {
137                        panic!("Cannot instantiate a templated file unsliced!")
138                    } else if input_raw_sliced.is_some() {
139                        panic!("Templated file was not sliced, but not has raw slices.")
140                    } else {
141                        (
142                            vec![TemplatedFileSlice::new(
143                                "literal",
144                                0..source_str.len(),
145                                0..source_str.len(),
146                            )],
147                            vec![RawFileSlice::new(
148                                source_str.clone(),
149                                "literal".to_string(),
150                                0,
151                                None,
152                                None,
153                            )],
154                        )
155                    }
156                }
157                Some(sliced_file) => {
158                    if let Some(raw_sliced) = input_raw_sliced {
159                        (sliced_file, raw_sliced)
160                    } else {
161                        panic!("Templated file was sliced, but not raw.")
162                    }
163                }
164            };
165
166        // Precalculate newlines, character positions.
167        let source_newlines: Vec<usize> = iter_indices_of_newlines(source_str.as_str()).collect();
168        let templated_newlines: Vec<usize> =
169            iter_indices_of_newlines(templated_str.as_str()).collect();
170
171        // Consistency check raw string and slices.
172        let mut pos = 0;
173        for rfs in &raw_sliced {
174            if rfs.source_idx != pos {
175                panic!(
176                    "TemplatedFile. Consistency fail on running source length. {} != {}",
177                    pos, rfs.source_idx
178                )
179            }
180            pos += rfs.raw.len();
181        }
182        if pos != source_str.len() {
183            panic!(
184                "TemplatedFile. Consistency fail on final source length. {} != {}",
185                pos,
186                source_str.len()
187            )
188        }
189
190        // Consistency check templated string and slices.
191        let mut previous_slice: Option<&TemplatedFileSlice> = None;
192        let mut outer_tfs: Option<&TemplatedFileSlice> = None;
193        for tfs in &sliced_file {
194            match &previous_slice {
195                Some(previous_slice) => {
196                    if tfs.templated_slice.start != previous_slice.templated_slice.end {
197                        return Err(SQLFluffSkipFile::new(
198                            "Templated slices found to be non-contiguous.".to_string(),
199                        ));
200                        // TODO Make this nicer again
201                        // format!(
202                        //     "Templated slices found to be non-contiguous.
203                        // {:?} (starting {:?}) does not follow {:?} (starting
204                        // {:?})",
205                        //     tfs.templated_slice,
206                        //     templated_str[tfs.templated_slice],
207                        //     previous_slice.templated_slice,
208                        //     templated_str[previous_slice.templated_slice],
209                        // )
210                    }
211                }
212                None => {
213                    if tfs.templated_slice.start != 0 {
214                        return Err(SQLFluffSkipFile::new(format!(
215                            "First templated slice does not start at 0, (found slice {:?})",
216                            tfs.templated_slice
217                        )));
218                    }
219                }
220            }
221            previous_slice = Some(tfs);
222            outer_tfs = Some(tfs)
223        }
224        if !sliced_file.is_empty() && input_templated_str.is_some() {
225            if let Some(outer_tfs) = outer_tfs {
226                if outer_tfs.templated_slice.end != templated_str.len() {
227                    return Err(SQLFluffSkipFile::new(format!(
228                        "Last templated slice does not end at end of string, (found slice {:?})",
229                        outer_tfs.templated_slice
230                    )));
231                }
232            }
233        }
234
235        Ok(TemplatedFileInner {
236            raw_sliced,
237            source_newlines,
238            templated_newlines,
239            source_str: source_str.clone(),
240            sliced_file,
241            name: f_name,
242            templated_str: Some(templated_str),
243        })
244    }
245
246    /// Return true if there's a templated file.
247    pub fn is_templated(&self) -> bool {
248        self.templated_str.is_some()
249    }
250
251    /// Get the line number and position of a point in the source file.
252    /// Args:
253    ///  - char_pos: The character position in the relevant file.
254    ///  - source: Are we checking the source file (as opposed to the templated
255    ///    file)
256    ///
257    /// Returns: line_number, line_position
258    pub fn get_line_pos_of_char_pos(&self, char_pos: usize, source: bool) -> (usize, usize) {
259        let ref_str = if source {
260            &self.source_newlines
261        } else {
262            &self.templated_newlines
263        };
264        match ref_str.binary_search(&char_pos) {
265            Ok(nl_idx) | Err(nl_idx) => {
266                if nl_idx > 0 {
267                    (nl_idx + 1, char_pos - ref_str[nl_idx - 1])
268                } else {
269                    // NB: line_pos is char_pos + 1 because character position is 0-indexed,
270                    // but the line position is 1-indexed.
271                    (1, char_pos + 1)
272                }
273            }
274        }
275    }
276
277    /// Create TemplatedFile from a string.
278    pub fn from_string(raw: SmolStr) -> TemplatedFile {
279        // TODO: Might need to deal with this unwrap
280        TemplatedFile::new(raw.into(), "<string>".to_string(), None, None, None).unwrap()
281    }
282
283    /// Get templated string
284    pub fn templated(&self) -> &str {
285        self.templated_str.as_deref().unwrap()
286    }
287
288    pub fn source_only_slices(&self) -> Vec<RawFileSlice> {
289        let mut ret_buff = vec![];
290        for element in &self.raw_sliced {
291            if element.is_source_only_slice() {
292                ret_buff.push(element.clone());
293            }
294        }
295        ret_buff
296    }
297
298    pub fn find_slice_indices_of_templated_pos(
299        &self,
300        templated_pos: usize,
301        start_idx: Option<usize>,
302        inclusive: Option<bool>,
303    ) -> Option<(usize, usize)> {
304        let start_idx = start_idx.unwrap_or(0);
305        let inclusive = inclusive.unwrap_or(true);
306
307        let mut first_idx: Option<usize> = None;
308        let mut last_idx = start_idx;
309
310        // Work through the sliced file, starting at the start_idx if given
311        // as an optimisation hint. The sliced_file is a list of TemplatedFileSlice
312        // which reference parts of the templated file and where they exist in the
313        // source.
314        for (idx, elem) in self.sliced_file[start_idx..self.sliced_file.len()]
315            .iter()
316            .enumerate()
317        {
318            last_idx = idx + start_idx;
319            if elem.templated_slice.end >= templated_pos {
320                if first_idx.is_none() {
321                    first_idx = Some(idx + start_idx);
322                }
323
324                if elem.templated_slice.start > templated_pos
325                    || (!inclusive && elem.templated_slice.end >= templated_pos)
326                {
327                    break;
328                }
329            }
330        }
331
332        // If we got to the end add another index
333        if last_idx == self.sliced_file.len() - 1 {
334            last_idx += 1;
335        }
336
337        first_idx.map(|first_idx| (first_idx, last_idx))
338    }
339
340    /// Convert a template slice to a source slice.
341    pub fn templated_slice_to_source_slice(
342        &self,
343        template_slice: Range<usize>,
344    ) -> Result<Range<usize>, String> {
345        if self.sliced_file.is_empty() {
346            return Ok(template_slice);
347        }
348
349        let sliced_file = self.sliced_file.clone();
350
351        let (ts_start_sf_start, ts_start_sf_stop) = self
352            .find_slice_indices_of_templated_pos(template_slice.start, None, None)
353            .ok_or("Position not found in templated file")?;
354
355        let ts_start_subsliced_file = &sliced_file[ts_start_sf_start..ts_start_sf_stop];
356
357        // Work out the insertion point
358        let mut insertion_point: isize = -1;
359        for elem in ts_start_subsliced_file.iter() {
360            // Do slice starts and ends
361            for &slice_elem in ["start", "stop"].iter() {
362                let elem_val = match slice_elem {
363                    "start" => elem.templated_slice.start,
364                    "stop" => elem.templated_slice.end,
365                    _ => panic!("Unexpected slice_elem"),
366                };
367
368                if elem_val == template_slice.start {
369                    let point = if slice_elem == "start" {
370                        elem.source_slice.start
371                    } else {
372                        elem.source_slice.end
373                    };
374
375                    let point: isize = point.try_into().unwrap();
376                    if insertion_point < 0 || point < insertion_point {
377                        insertion_point = point;
378                    }
379                    // We don't break here, because we might find ANOTHER
380                    // later which is actually earlier.
381                }
382            }
383        }
384
385        // Zero length slice.
386        if template_slice.start == template_slice.end {
387            // Is it on a join?
388            return if insertion_point >= 0 {
389                Ok(zero_slice(insertion_point.try_into().unwrap()))
390                // It's within a segment.
391            } else if !ts_start_subsliced_file.is_empty()
392                && ts_start_subsliced_file[0].slice_type == "literal"
393            {
394                let offset =
395                    template_slice.start - ts_start_subsliced_file[0].templated_slice.start;
396                Ok(zero_slice(
397                    ts_start_subsliced_file[0].source_slice.start + offset,
398                ))
399            } else {
400                Err(format!(
401                    "Attempting a single length slice within a templated section! {template_slice:?} within \
402                     {ts_start_subsliced_file:?}."
403                ))
404            };
405        }
406
407        let (ts_stop_sf_start, ts_stop_sf_stop) = self
408            .find_slice_indices_of_templated_pos(template_slice.end, None, Some(false))
409            .ok_or("Position not found in templated file")?;
410
411        let mut ts_start_sf_start = ts_start_sf_start;
412        if insertion_point >= 0 {
413            for elem in &sliced_file[ts_start_sf_start..] {
414                let insertion_point: usize = insertion_point.try_into().unwrap();
415                if elem.source_slice.start != insertion_point {
416                    ts_start_sf_start += 1;
417                } else {
418                    break;
419                }
420            }
421        }
422
423        let subslices = &sliced_file[usize::min(ts_start_sf_start, ts_stop_sf_start)
424            ..usize::max(ts_start_sf_stop, ts_stop_sf_stop)];
425
426        let start_slices = if ts_start_sf_start == ts_start_sf_stop {
427            return match ts_start_sf_start.cmp(&sliced_file.len()) {
428                Ordering::Greater => {
429                    panic!("Starting position higher than sliced file position")
430                }
431                Ordering::Less => Ok(sliced_file[1].source_slice.clone()),
432                Ordering::Equal => Ok(sliced_file.last().unwrap().source_slice.clone()),
433            };
434        } else {
435            &sliced_file[ts_start_sf_start..ts_start_sf_stop]
436        };
437
438        let stop_slices = if ts_stop_sf_start == ts_stop_sf_stop {
439            vec![sliced_file[ts_stop_sf_start].clone()]
440        } else {
441            sliced_file[ts_stop_sf_start..ts_stop_sf_stop].to_vec()
442        };
443
444        let source_start: isize = if insertion_point >= 0 {
445            insertion_point
446        } else if start_slices[0].slice_type == "literal" {
447            let offset = template_slice.start - start_slices[0].templated_slice.start;
448            (start_slices[0].source_slice.start + offset)
449                .try_into()
450                .unwrap()
451        } else {
452            start_slices[0].source_slice.start.try_into().unwrap()
453        };
454
455        let source_stop = if stop_slices.last().unwrap().slice_type == "literal" {
456            let offset = stop_slices.last().unwrap().templated_slice.end - template_slice.end;
457            stop_slices.last().unwrap().source_slice.end - offset
458        } else {
459            stop_slices.last().unwrap().source_slice.end
460        };
461
462        let source_slice;
463        if source_start > source_stop.try_into().unwrap() {
464            let mut source_start = usize::MAX;
465            let mut source_stop = 0;
466            for elem in subslices {
467                source_start = usize::min(source_start, elem.source_slice.start);
468                source_stop = usize::max(source_stop, elem.source_slice.end);
469            }
470            source_slice = source_start..source_stop;
471        } else {
472            source_slice = source_start.try_into().unwrap()..source_stop;
473        }
474
475        Ok(source_slice)
476    }
477
478    ///  Work out whether a slice of the source file is a literal or not.
479    pub fn is_source_slice_literal(&self, source_slice: &Range<usize>) -> bool {
480        // No sliced file? Everything is literal
481        if self.raw_sliced.is_empty() {
482            return true;
483        };
484
485        // Zero length slice. It's a literal, because it's definitely not templated.
486        if source_slice.start == source_slice.end {
487            return true;
488        };
489
490        let mut is_literal = true;
491        for raw_slice in &self.raw_sliced {
492            // Reset if we find a literal and we're up to the start
493            // otherwise set false.
494            if raw_slice.source_idx <= source_slice.start {
495                is_literal = raw_slice.slice_type == "literal";
496            } else if raw_slice.source_idx >= source_slice.end {
497                break;
498            } else if raw_slice.slice_type != "literal" {
499                is_literal = false;
500            };
501        }
502        is_literal
503    }
504
505    /// Return a list of the raw slices spanning a set of indices.
506    pub(crate) fn raw_slices_spanning_source_slice(
507        &self,
508        source_slice: &Range<usize>,
509    ) -> Vec<RawFileSlice> {
510        // Special case: The source_slice is at the end of the file.
511        let last_raw_slice = self.raw_sliced.last().unwrap();
512        if source_slice.start >= last_raw_slice.source_idx + last_raw_slice.raw.len() {
513            return Vec::new();
514        }
515
516        // First find the start index
517        let mut raw_slice_idx = 0;
518        // Move the raw pointer forward to the start of this patch
519        while raw_slice_idx + 1 < self.raw_sliced.len()
520            && self.raw_sliced[raw_slice_idx + 1].source_idx <= source_slice.start
521        {
522            raw_slice_idx += 1;
523        }
524
525        // Find slice index of the end of this patch.
526        let mut slice_span = 1;
527        while raw_slice_idx + slice_span < self.raw_sliced.len()
528            && self.raw_sliced[raw_slice_idx + slice_span].source_idx < source_slice.end
529        {
530            slice_span += 1;
531        }
532
533        // Return the raw slices
534        self.raw_sliced[raw_slice_idx..(raw_slice_idx + slice_span)].to_vec()
535    }
536}
537
538/// Find the indices of all newlines in a string.
539pub fn iter_indices_of_newlines(raw_str: &str) -> impl Iterator<Item = usize> + '_ {
540    // TODO: This may be optimize-able by not doing it all up front.
541    raw_str.match_indices('\n').map(|(idx, _)| idx)
542}
543
544#[cfg_attr(feature = "stringify", derive(Serialize, Deserialize))]
545#[derive(Debug, PartialEq, Eq, Clone, Hash)]
546pub enum RawFileSliceType {
547    Comment,
548    BlockEnd,
549    BlockStart,
550    BlockMid,
551}
552
553/// A slice referring to a raw file.
554#[cfg_attr(feature = "stringify", derive(Serialize, Deserialize))]
555#[derive(Debug, PartialEq, Eq, Clone, Hash)]
556pub struct RawFileSlice {
557    /// Source string
558    raw: String,
559    pub(crate) slice_type: String,
560    /// Offset from beginning of source string
561    pub source_idx: usize,
562    slice_subtype: Option<RawFileSliceType>,
563    /// Block index, incremented on start or end block tags, e.g. "if", "for"
564    block_idx: usize,
565}
566
567impl RawFileSlice {
568    pub fn new(
569        raw: String,
570        slice_type: String,
571        source_idx: usize,
572        slice_subtype: Option<RawFileSliceType>,
573        block_idx: Option<usize>,
574    ) -> Self {
575        Self {
576            raw,
577            slice_type,
578            source_idx,
579            slice_subtype,
580            block_idx: block_idx.unwrap_or(0),
581        }
582    }
583}
584
585impl RawFileSlice {
586    /// Return the closing index of this slice.
587    fn end_source_idx(&self) -> usize {
588        self.source_idx + self.raw.len()
589    }
590
591    /// Return the a slice object for this slice.
592    pub fn source_slice(&self) -> Range<usize> {
593        self.source_idx..self.end_source_idx()
594    }
595
596    /// Based on its slice_type, does it only appear in the *source*?
597    /// There are some slice types which are automatically source only.
598    /// There are *also* some which are source only because they render
599    /// to an empty string.
600    fn is_source_only_slice(&self) -> bool {
601        // TODO: should any new logic go here?. Slice Type could probably go from String
602        // To Enum
603        matches!(
604            self.slice_type.as_str(),
605            "comment" | "block_end" | "block_start" | "block_mid"
606        )
607    }
608}
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613
614    #[test]
615    fn test_indices_of_newlines() {
616        vec![
617            ("", vec![]),
618            ("foo", vec![]),
619            ("foo\nbar", vec![3]),
620            ("\nfoo\n\nbar\nfoo\n\nbar\n", vec![0, 4, 5, 9, 13, 14, 18]),
621        ]
622        .into_iter()
623        .for_each(|(in_str, expected)| {
624            assert_eq!(
625                expected,
626                iter_indices_of_newlines(in_str).collect::<Vec<usize>>()
627            )
628        });
629    }
630
631    // const SIMPLE_SOURCE_STR: &str = "01234\n6789{{foo}}fo\nbarss";
632    // const SIMPLE_TEMPLATED_STR: &str = "01234\n6789x\nfo\nbarfss";
633
634    fn simple_sliced_file() -> Vec<TemplatedFileSlice> {
635        vec![
636            TemplatedFileSlice::new("literal", 0..10, 0..10),
637            TemplatedFileSlice::new("templated", 10..17, 10..12),
638            TemplatedFileSlice::new("literal", 17..25, 12..20),
639        ]
640    }
641
642    fn simple_raw_sliced_file() -> [RawFileSlice; 3] {
643        [
644            RawFileSlice::new("x".repeat(10), "literal".to_string(), 0, None, None),
645            RawFileSlice::new("x".repeat(7), "templated".to_string(), 10, None, None),
646            RawFileSlice::new("x".repeat(8), "literal".to_string(), 17, None, None),
647        ]
648    }
649
650    fn complex_sliced_file() -> Vec<TemplatedFileSlice> {
651        vec![
652            TemplatedFileSlice::new("literal", 0..13, 0..13),
653            TemplatedFileSlice::new("comment", 13..29, 13..13),
654            TemplatedFileSlice::new("literal", 29..44, 13..28),
655            TemplatedFileSlice::new("block_start", 44..68, 28..28),
656            TemplatedFileSlice::new("literal", 68..81, 28..41),
657            TemplatedFileSlice::new("templated", 81..86, 41..42),
658            TemplatedFileSlice::new("literal", 86..110, 42..66),
659            TemplatedFileSlice::new("templated", 68..86, 66..76),
660            TemplatedFileSlice::new("literal", 68..81, 76..89),
661            TemplatedFileSlice::new("templated", 81..86, 89..90),
662            TemplatedFileSlice::new("literal", 86..110, 90..114),
663            TemplatedFileSlice::new("templated", 68..86, 114..125),
664            TemplatedFileSlice::new("literal", 68..81, 125..138),
665            TemplatedFileSlice::new("templated", 81..86, 138..139),
666            TemplatedFileSlice::new("literal", 86..110, 139..163),
667            TemplatedFileSlice::new("templated", 110..123, 163..166),
668            TemplatedFileSlice::new("literal", 123..132, 166..175),
669            TemplatedFileSlice::new("block_end", 132..144, 175..175),
670            TemplatedFileSlice::new("literal", 144..155, 175..186),
671            TemplatedFileSlice::new("block_start", 155..179, 186..186),
672            TemplatedFileSlice::new("literal", 179..189, 186..196),
673            TemplatedFileSlice::new("templated", 189..194, 196..197),
674            TemplatedFileSlice::new("literal", 194..203, 197..206),
675            TemplatedFileSlice::new("literal", 179..189, 206..216),
676            TemplatedFileSlice::new("templated", 189..194, 216..217),
677            TemplatedFileSlice::new("literal", 194..203, 217..226),
678            TemplatedFileSlice::new("literal", 179..189, 226..236),
679            TemplatedFileSlice::new("templated", 189..194, 236..237),
680            TemplatedFileSlice::new("literal", 194..203, 237..246),
681            TemplatedFileSlice::new("block_end", 203..215, 246..246),
682            TemplatedFileSlice::new("literal", 215..230, 246..261),
683        ]
684    }
685
686    fn complex_raw_sliced_file() -> Vec<RawFileSlice> {
687        vec![
688            RawFileSlice::new(
689                "x".repeat(13).to_string(),
690                "literal".to_string(),
691                0,
692                None,
693                None,
694            ),
695            RawFileSlice::new(
696                "x".repeat(16).to_string(),
697                "comment".to_string(),
698                13,
699                None,
700                None,
701            ),
702            RawFileSlice::new(
703                "x".repeat(15).to_string(),
704                "literal".to_string(),
705                29,
706                None,
707                None,
708            ),
709            RawFileSlice::new(
710                "x".repeat(24).to_string(),
711                "block_start".to_string(),
712                44,
713                None,
714                None,
715            ),
716            RawFileSlice::new(
717                "x".repeat(13).to_string(),
718                "literal".to_string(),
719                68,
720                None,
721                None,
722            ),
723            RawFileSlice::new(
724                "x".repeat(5).to_string(),
725                "templated".to_string(),
726                81,
727                None,
728                None,
729            ),
730            RawFileSlice::new(
731                "x".repeat(24).to_string(),
732                "literal".to_string(),
733                86,
734                None,
735                None,
736            ),
737            RawFileSlice::new(
738                "x".repeat(13).to_string(),
739                "templated".to_string(),
740                110,
741                None,
742                None,
743            ),
744            RawFileSlice::new(
745                "x".repeat(9).to_string(),
746                "literal".to_string(),
747                123,
748                None,
749                None,
750            ),
751            RawFileSlice::new(
752                "x".repeat(12).to_string(),
753                "block_end".to_string(),
754                132,
755                None,
756                None,
757            ),
758            RawFileSlice::new(
759                "x".repeat(11).to_string(),
760                "literal".to_string(),
761                144,
762                None,
763                None,
764            ),
765            RawFileSlice::new(
766                "x".repeat(24).to_string(),
767                "block_start".to_string(),
768                155,
769                None,
770                None,
771            ),
772            RawFileSlice::new(
773                "x".repeat(10).to_string(),
774                "literal".to_string(),
775                179,
776                None,
777                None,
778            ),
779            RawFileSlice::new(
780                "x".repeat(5).to_string(),
781                "templated".to_string(),
782                189,
783                None,
784                None,
785            ),
786            RawFileSlice::new(
787                "x".repeat(9).to_string(),
788                "literal".to_string(),
789                194,
790                None,
791                None,
792            ),
793            RawFileSlice::new(
794                "x".repeat(12).to_string(),
795                "block_end".to_string(),
796                203,
797                None,
798                None,
799            ),
800            RawFileSlice::new(
801                "x".repeat(15).to_string(),
802                "literal".to_string(),
803                215,
804                None,
805                None,
806            ),
807        ]
808    }
809
810    struct FileKwargs {
811        f_name: String,
812        source_str: String,
813        templated_str: Option<String>,
814        sliced_file: Vec<TemplatedFileSlice>,
815        raw_sliced_file: Vec<RawFileSlice>,
816    }
817
818    fn simple_file_kwargs() -> FileKwargs {
819        FileKwargs {
820            f_name: "test.sql".to_string(),
821            source_str: "01234\n6789{{foo}}fo\nbarss".to_string(),
822            templated_str: Some("01234\n6789x\nfo\nbarss".to_string()),
823            sliced_file: simple_sliced_file().to_vec(),
824            raw_sliced_file: simple_raw_sliced_file().to_vec(),
825        }
826    }
827
828    fn complex_file_kwargs() -> FileKwargs {
829        FileKwargs {
830            f_name: "test.sql".to_string(),
831            source_str: complex_raw_sliced_file()
832                .iter()
833                .fold(String::new(), |acc, x| acc + &x.raw),
834            templated_str: None,
835            sliced_file: complex_sliced_file().to_vec(),
836            raw_sliced_file: complex_raw_sliced_file().to_vec(),
837        }
838    }
839
840    #[test]
841    /// Test TemplatedFile.get_line_pos_of_char_pos.
842    fn test_templated_file_get_line_pos_of_char_pos() {
843        let tests = [
844            (simple_file_kwargs(), 0, 1, 1),
845            (simple_file_kwargs(), 20, 3, 1),
846            (simple_file_kwargs(), 24, 3, 5),
847        ];
848
849        for test in tests {
850            let kwargs = test.0;
851
852            let tf = TemplatedFile::new(
853                kwargs.source_str,
854                kwargs.f_name,
855                kwargs.templated_str,
856                Some(kwargs.sliced_file),
857                Some(kwargs.raw_sliced_file),
858            )
859            .unwrap();
860
861            let (res_line_no, res_line_pos) = tf.get_line_pos_of_char_pos(test.1, true);
862
863            assert_eq!(res_line_no, test.2);
864            assert_eq!(res_line_pos, test.3);
865        }
866    }
867
868    #[test]
869    fn test_templated_file_find_slice_indices_of_templated_pos() {
870        let tests = vec![
871            // "templated_position,inclusive,file_slices,sliced_idx_start,sliced_idx_stop",
872            // TODO Fix these
873            // (100, true, complex_file_kwargs(), 10, 11),
874            // (13, true, complex_file_kwargs(), 0, 3),
875            // (28, true, complex_file_kwargs(), 2, 5),
876            // # Check end slicing.
877            (12, true, simple_file_kwargs(), 1, 3),
878            (20, true, simple_file_kwargs(), 2, 3),
879            // Check inclusivity
880            // (13, false, complex_file_kwargs(), 0, 1),
881        ];
882
883        for test in tests {
884            let args = test.2;
885
886            let file = TemplatedFile::new(
887                args.source_str,
888                args.f_name,
889                args.templated_str,
890                Some(args.sliced_file),
891                Some(args.raw_sliced_file),
892            )
893            .unwrap();
894
895            let (res_start, res_stop) = file
896                .find_slice_indices_of_templated_pos(test.0, None, Some(test.1))
897                .unwrap();
898
899            assert_eq!(res_start, test.3);
900            assert_eq!(res_stop, test.4);
901        }
902    }
903
904    #[test]
905    /// Test TemplatedFile.templated_slice_to_source_slice
906    fn test_templated_file_templated_slice_to_source_slice() {
907        let test_cases = vec![
908            // Simple example
909            (
910                5..10,
911                5..10,
912                true,
913                FileKwargs {
914                    sliced_file: vec![TemplatedFileSlice::new("literal", 0..20, 0..20)],
915                    raw_sliced_file: vec![RawFileSlice::new(
916                        "x".repeat(20),
917                        "literal".to_string(),
918                        0,
919                        None,
920                        None,
921                    )],
922                    source_str: "x".repeat(20),
923                    f_name: "foo.sql".to_string(),
924                    templated_str: None,
925                },
926            ),
927            // Trimming the end of a literal (with things that follow).
928            (10..13, 10..13, true, complex_file_kwargs()),
929            // // Unrealistic, but should still work
930            (
931                5..10,
932                55..60,
933                true,
934                FileKwargs {
935                    sliced_file: vec![TemplatedFileSlice::new("literal", 50..70, 0..20)],
936                    raw_sliced_file: vec![
937                        RawFileSlice::new("x".repeat(50), "literal".to_string(), 0, None, None),
938                        RawFileSlice::new("x".repeat(20), "literal".to_string(), 50, None, None),
939                    ],
940                    source_str: "x".repeat(70),
941                    f_name: "foo.sql".to_string(),
942                    templated_str: None,
943                },
944            ),
945            // // Spanning a template
946            (5..15, 5..20, false, simple_file_kwargs()),
947            // // Handling templated
948            (
949                5..15,
950                0..25,
951                false,
952                FileKwargs {
953                    sliced_file: simple_file_kwargs()
954                        .sliced_file
955                        .iter()
956                        .map(|slc| {
957                            TemplatedFileSlice::new(
958                                "templated",
959                                slc.source_slice.clone(),
960                                slc.templated_slice.clone(),
961                            )
962                        })
963                        .collect(),
964                    raw_sliced_file: simple_file_kwargs()
965                        .raw_sliced_file
966                        .iter()
967                        .map(|slc| {
968                            RawFileSlice::new(
969                                slc.raw.to_string(),
970                                "templated".to_string(),
971                                slc.source_idx,
972                                None,
973                                None,
974                            )
975                        })
976                        .collect(),
977                    ..simple_file_kwargs()
978                },
979            ),
980            // // Handling single length slices
981            (10..10, 10..10, true, simple_file_kwargs()),
982            (12..12, 17..17, true, simple_file_kwargs()),
983            // // Dealing with single length elements
984            (
985                20..20,
986                25..25,
987                true,
988                FileKwargs {
989                    sliced_file: simple_file_kwargs()
990                        .sliced_file
991                        .into_iter()
992                        .chain(vec![TemplatedFileSlice::new("comment", 25..35, 20..20)])
993                        .collect(),
994                    raw_sliced_file: simple_file_kwargs()
995                        .raw_sliced_file
996                        .into_iter()
997                        .chain(vec![RawFileSlice::new(
998                            "x".repeat(10),
999                            "comment".to_string(),
1000                            25,
1001                            None,
1002                            None,
1003                        )])
1004                        .collect(),
1005                    source_str: simple_file_kwargs().source_str.to_string() + &"x".repeat(10),
1006                    ..simple_file_kwargs()
1007                },
1008            ),
1009            // // Just more test coverage
1010            (43..43, 87..87, true, complex_file_kwargs()),
1011            (13..13, 13..13, true, complex_file_kwargs()),
1012            (186..186, 155..155, true, complex_file_kwargs()),
1013            // Backward slicing.
1014            (
1015                100..130,
1016                // NB This actually would reference the wrong way around if we
1017                // just take the points. Here we should handle it gracefully.
1018                68..110,
1019                false,
1020                complex_file_kwargs(),
1021            ),
1022        ];
1023
1024        for (in_slice, out_slice, is_literal, tf_kwargs) in test_cases {
1025            let file = TemplatedFile::new(
1026                tf_kwargs.source_str,
1027                tf_kwargs.f_name,
1028                tf_kwargs.templated_str,
1029                Some(tf_kwargs.sliced_file),
1030                Some(tf_kwargs.raw_sliced_file),
1031            )
1032            .unwrap();
1033
1034            let source_slice = file.templated_slice_to_source_slice(in_slice).unwrap();
1035            let literal_test = file.is_source_slice_literal(&source_slice);
1036
1037            assert_eq!((is_literal, source_slice), (literal_test, out_slice));
1038        }
1039    }
1040
1041    #[test]
1042    /// Test TemplatedFile.source_only_slices
1043    fn test_templated_file_source_only_slices() {
1044        let test_cases = vec![
1045            // Comment example
1046            (
1047                TemplatedFile::new(
1048                    format!("{}{}{}", "a".repeat(10), "{# b #}", "a".repeat(10)),
1049                    "test".to_string(),
1050                    None,
1051                    Some(vec![
1052                        TemplatedFileSlice::new("literal", 0..10, 0..10),
1053                        TemplatedFileSlice::new("templated", 10..17, 10..10),
1054                        TemplatedFileSlice::new("literal", 17..27, 10..20),
1055                    ]),
1056                    Some(vec![
1057                        RawFileSlice::new(
1058                            "a".repeat(10).to_string(),
1059                            "literal".to_string(),
1060                            0,
1061                            None,
1062                            None,
1063                        ),
1064                        RawFileSlice::new(
1065                            "{# b #}".to_string(),
1066                            "comment".to_string(),
1067                            10,
1068                            None,
1069                            None,
1070                        ),
1071                        RawFileSlice::new(
1072                            "a".repeat(10).to_string(),
1073                            "literal".to_string(),
1074                            17,
1075                            None,
1076                            None,
1077                        ),
1078                    ]),
1079                )
1080                .unwrap(),
1081                vec![RawFileSlice::new(
1082                    "{# b #}".to_string(),
1083                    "comment".to_string(),
1084                    10,
1085                    None,
1086                    None,
1087                )],
1088            ),
1089            // Template tags aren't source only.
1090            (
1091                TemplatedFile::new(
1092                    "aaa{{ b }}aaa".to_string(),
1093                    "test".to_string(),
1094                    None,
1095                    Some(vec![
1096                        TemplatedFileSlice::new("literal", 0..3, 0..3),
1097                        TemplatedFileSlice::new("templated", 3..10, 3..6),
1098                        TemplatedFileSlice::new("literal", 10..13, 6..9),
1099                    ]),
1100                    Some(vec![
1101                        RawFileSlice::new("aaa".to_string(), "literal".to_string(), 0, None, None),
1102                        RawFileSlice::new(
1103                            "{{ b }}".to_string(),
1104                            "templated".to_string(),
1105                            3,
1106                            None,
1107                            None,
1108                        ),
1109                        RawFileSlice::new("aaa".to_string(), "literal".to_string(), 10, None, None),
1110                    ]),
1111                )
1112                .unwrap(),
1113                vec![],
1114            ),
1115        ];
1116
1117        for (file, expected) in test_cases {
1118            assert_eq!(file.source_only_slices(), expected, "Failed for {:?}", file);
1119        }
1120    }
1121}