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