Skip to main content

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#[cfg_attr(feature = "stringify", derive(Serialize, Deserialize))]
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum TemplateSliceKind {
16    Literal,
17    Templated,
18    Comment,
19    BlockStart,
20    BlockMid,
21    BlockEnd,
22}
23
24impl TemplateSliceKind {
25    pub const fn as_str(self) -> &'static str {
26        match self {
27            Self::Literal => "literal",
28            Self::Templated => "templated",
29            Self::Comment => "comment",
30            Self::BlockStart => "block_start",
31            Self::BlockMid => "block_mid",
32            Self::BlockEnd => "block_end",
33        }
34    }
35
36    pub const fn is_source_only(self) -> bool {
37        matches!(
38            self,
39            Self::Comment | Self::BlockEnd | Self::BlockStart | Self::BlockMid
40        )
41    }
42
43    pub fn from_slice_type(value: &str) -> Result<Self, String> {
44        match value {
45            "literal" => Ok(Self::Literal),
46            "templated" => Ok(Self::Templated),
47            "comment" => Ok(Self::Comment),
48            "block_start" => Ok(Self::BlockStart),
49            "block_mid" => Ok(Self::BlockMid),
50            "block_end" => Ok(Self::BlockEnd),
51            _ => Err(format!("Unknown template slice kind '{value}'")),
52        }
53    }
54}
55
56/// A slice referring to a templated file.
57#[cfg_attr(feature = "stringify", derive(Serialize))]
58#[derive(Debug, Clone, PartialEq, Eq, Hash)]
59pub struct TemplatedFileSlice {
60    pub slice_type: TemplateSliceKind,
61    pub source_slice: Range<usize>,
62    pub templated_slice: Range<usize>,
63}
64
65impl TemplatedFileSlice {
66    pub fn new(
67        slice_type: TemplateSliceKind,
68        source_slice: Range<usize>,
69        templated_slice: Range<usize>,
70    ) -> Self {
71        Self {
72            slice_type,
73            source_slice,
74            templated_slice,
75        }
76    }
77
78    pub fn new_typed(
79        slice_type: TemplateSliceKind,
80        source_slice: Range<usize>,
81        templated_slice: Range<usize>,
82    ) -> Self {
83        Self::new(slice_type, source_slice, templated_slice)
84    }
85
86    pub const fn slice_kind(&self) -> TemplateSliceKind {
87        self.slice_type
88    }
89
90    pub fn has_slice_kind(&self, kind: TemplateSliceKind) -> bool {
91        self.slice_kind() == kind
92    }
93}
94
95/// A templated SQL file.
96///
97/// This is the response of a `templater`'s `.process()` method
98/// and contains both references to the original file and also
99/// the capability to split up that file when lexing.
100#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)]
101pub struct TemplatedFile {
102    inner: Arc<TemplatedFileInner>,
103}
104
105impl TemplatedFile {
106    pub fn new(
107        source_str: String,
108        name: String,
109        input_templated_str: Option<String>,
110        sliced_file: Option<Vec<TemplatedFileSlice>>,
111        input_raw_sliced: Option<Vec<RawFileSlice>>,
112    ) -> Result<TemplatedFile, SQLFluffSkipFile> {
113        Ok(TemplatedFile {
114            inner: Arc::new(TemplatedFileInner::new(
115                source_str,
116                name,
117                input_templated_str,
118                sliced_file,
119                input_raw_sliced,
120            )?),
121        })
122    }
123
124    pub fn name(&self) -> &str {
125        &self.inner.name
126    }
127
128    #[cfg(feature = "stringify")]
129    pub fn to_yaml(&self) -> String {
130        let inner = &*self.inner;
131        serde_yaml::to_string(inner).unwrap()
132    }
133}
134
135impl From<String> for TemplatedFile {
136    fn from(raw: String) -> Self {
137        TemplatedFile {
138            inner: Arc::new(
139                TemplatedFileInner::new(raw, "<string>".to_string(), None, None, None).unwrap(),
140            ),
141        }
142    }
143}
144
145impl From<&str> for TemplatedFile {
146    fn from(raw: &str) -> Self {
147        TemplatedFile {
148            inner: Arc::new(
149                TemplatedFileInner::new(raw.to_string(), "<string>".to_string(), None, None, None)
150                    .unwrap(),
151            ),
152        }
153    }
154}
155
156impl Deref for TemplatedFile {
157    type Target = TemplatedFileInner;
158
159    fn deref(&self) -> &Self::Target {
160        &self.inner
161    }
162}
163
164#[cfg_attr(feature = "stringify", derive(Serialize))]
165#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)]
166pub struct TemplatedFileInner {
167    pub source_str: String,
168    name: String,
169    pub templated_str: Option<String>,
170    source_newlines: Vec<usize>,
171    templated_newlines: Vec<usize>,
172    raw_sliced: Vec<RawFileSlice>,
173    pub sliced_file: Vec<TemplatedFileSlice>,
174}
175
176impl TemplatedFileInner {
177    /// Initialise the TemplatedFile.
178    /// If no templated_str is provided then we assume that
179    /// the file is NOT templated and that the templated view
180    /// is the same as the source view.
181    pub fn new(
182        source_str: String,
183        f_name: String,
184        input_templated_str: Option<String>,
185        sliced_file: Option<Vec<TemplatedFileSlice>>,
186        input_raw_sliced: Option<Vec<RawFileSlice>>,
187    ) -> Result<TemplatedFileInner, SQLFluffSkipFile> {
188        // Assume that no sliced_file, means the file is not templated.
189        // TODO Will this not always be Some and so type can avoid Option?
190        let templated_str = input_templated_str.clone().unwrap_or(source_str.clone());
191
192        let (sliced_file, raw_sliced): (Vec<TemplatedFileSlice>, Vec<RawFileSlice>) =
193            match sliced_file {
194                None => {
195                    if templated_str != source_str {
196                        panic!("Cannot instantiate a templated file unsliced!")
197                    } else if input_raw_sliced.is_some() {
198                        panic!("Templated file was not sliced, but not has raw slices.")
199                    } else {
200                        (
201                            vec![TemplatedFileSlice::new_typed(
202                                TemplateSliceKind::Literal,
203                                0..source_str.len(),
204                                0..source_str.len(),
205                            )],
206                            vec![RawFileSlice::new_typed(
207                                source_str.clone(),
208                                TemplateSliceKind::Literal,
209                                0,
210                                None,
211                                None,
212                            )],
213                        )
214                    }
215                }
216                Some(sliced_file) => {
217                    if let Some(raw_sliced) = input_raw_sliced {
218                        (sliced_file, raw_sliced)
219                    } else {
220                        panic!("Templated file was sliced, but not raw.")
221                    }
222                }
223            };
224
225        // Precalculate newlines, character positions.
226        let source_newlines: Vec<usize> = iter_indices_of_newlines(source_str.as_str()).collect();
227        let templated_newlines: Vec<usize> =
228            iter_indices_of_newlines(templated_str.as_str()).collect();
229
230        // Consistency check raw string and slices.
231        let mut pos = 0;
232        for rfs in &raw_sliced {
233            if rfs.source_idx != pos {
234                panic!(
235                    "TemplatedFile. Consistency fail on running source length. {} != {}",
236                    pos, rfs.source_idx
237                )
238            }
239            pos += rfs.raw.len();
240        }
241        if pos != source_str.len() {
242            panic!(
243                "TemplatedFile. Consistency fail on final source length. {} != {}",
244                pos,
245                source_str.len()
246            )
247        }
248
249        // Consistency check templated string and slices.
250        let mut previous_slice: Option<&TemplatedFileSlice> = None;
251        let mut outer_tfs: Option<&TemplatedFileSlice> = None;
252        for tfs in &sliced_file {
253            match &previous_slice {
254                Some(previous_slice) => {
255                    if tfs.templated_slice.start != previous_slice.templated_slice.end {
256                        return Err(SQLFluffSkipFile::new(
257                            "Templated slices found to be non-contiguous.".to_string(),
258                        ));
259                        // TODO Make this nicer again
260                        // format!(
261                        //     "Templated slices found to be non-contiguous.
262                        // {:?} (starting {:?}) does not follow {:?} (starting
263                        // {:?})",
264                        //     tfs.templated_slice,
265                        //     templated_str[tfs.templated_slice],
266                        //     previous_slice.templated_slice,
267                        //     templated_str[previous_slice.templated_slice],
268                        // )
269                    }
270                }
271                None => {
272                    if tfs.templated_slice.start != 0 {
273                        return Err(SQLFluffSkipFile::new(format!(
274                            "First templated slice does not start at 0, (found slice {:?})",
275                            tfs.templated_slice
276                        )));
277                    }
278                }
279            }
280            previous_slice = Some(tfs);
281            outer_tfs = Some(tfs)
282        }
283        if !sliced_file.is_empty()
284            && input_templated_str.is_some()
285            && let Some(outer_tfs) = outer_tfs
286            && outer_tfs.templated_slice.end != templated_str.len()
287        {
288            return Err(SQLFluffSkipFile::new(format!(
289                "Last templated slice does not end at end of string, (found slice {:?})",
290                outer_tfs.templated_slice
291            )));
292        }
293
294        Ok(TemplatedFileInner {
295            raw_sliced,
296            source_newlines,
297            templated_newlines,
298            source_str: source_str.clone(),
299            sliced_file,
300            name: f_name,
301            templated_str: Some(templated_str),
302        })
303    }
304
305    /// Return true if there's a templated file.
306    pub fn is_templated(&self) -> bool {
307        self.templated_str.is_some()
308    }
309
310    /// Get the line number and position of a point in the source file.
311    /// Args:
312    ///  - char_pos: The character position in the relevant file.
313    ///  - source: Are we checking the source file (as opposed to the templated
314    ///    file)
315    ///
316    /// Returns: line_number, line_position
317    pub fn get_line_pos_of_char_pos(&self, char_pos: usize, source: bool) -> (usize, usize) {
318        let ref_str = if source {
319            &self.source_newlines
320        } else {
321            &self.templated_newlines
322        };
323        match ref_str.binary_search(&char_pos) {
324            Ok(nl_idx) | Err(nl_idx) => {
325                if nl_idx > 0 {
326                    (nl_idx + 1, char_pos - ref_str[nl_idx - 1])
327                } else {
328                    // NB: line_pos is char_pos + 1 because character position is 0-indexed,
329                    // but the line position is 1-indexed.
330                    (1, char_pos + 1)
331                }
332            }
333        }
334    }
335
336    /// Create TemplatedFile from a string.
337    pub fn from_string(raw: SmolStr) -> TemplatedFile {
338        // TODO: Might need to deal with this unwrap
339        TemplatedFile::new(raw.into(), "<string>".to_string(), None, None, None).unwrap()
340    }
341
342    /// Get templated string
343    pub fn templated(&self) -> &str {
344        self.templated_str.as_deref().unwrap()
345    }
346
347    pub fn source_only_slices(&self) -> Vec<RawFileSlice> {
348        let mut ret_buff = vec![];
349        for element in &self.raw_sliced {
350            if element.is_source_only_slice() {
351                ret_buff.push(element.clone());
352            }
353        }
354        ret_buff
355    }
356
357    /// Get all raw slices (template and literal).
358    pub fn raw_sliced(&self) -> &[RawFileSlice] {
359        &self.raw_sliced
360    }
361
362    pub fn find_slice_indices_of_templated_pos(
363        &self,
364        templated_pos: usize,
365        start_idx: Option<usize>,
366        inclusive: Option<bool>,
367    ) -> Option<(usize, usize)> {
368        let start_idx = start_idx.unwrap_or(0);
369        let inclusive = inclusive.unwrap_or(true);
370
371        let mut first_idx: Option<usize> = None;
372        let mut last_idx = start_idx;
373
374        // Work through the sliced file, starting at the start_idx if given
375        // as an optimisation hint. The sliced_file is a list of TemplatedFileSlice
376        // which reference parts of the templated file and where they exist in the
377        // source.
378        for (idx, elem) in self.sliced_file[start_idx..self.sliced_file.len()]
379            .iter()
380            .enumerate()
381        {
382            last_idx = idx + start_idx;
383            if elem.templated_slice.end >= templated_pos {
384                if first_idx.is_none() {
385                    first_idx = Some(idx + start_idx);
386                }
387
388                if elem.templated_slice.start > templated_pos
389                    || (!inclusive && elem.templated_slice.end >= templated_pos)
390                {
391                    break;
392                }
393            }
394        }
395
396        // If we got to the end add another index
397        if last_idx == self.sliced_file.len() - 1 {
398            last_idx += 1;
399        }
400
401        first_idx.map(|first_idx| (first_idx, last_idx))
402    }
403
404    /// Convert a template slice to a source slice.
405    pub fn templated_slice_to_source_slice(
406        &self,
407        template_slice: Range<usize>,
408    ) -> Result<Range<usize>, String> {
409        if self.sliced_file.is_empty() {
410            return Ok(template_slice);
411        }
412
413        let sliced_file = self.sliced_file.clone();
414
415        let (ts_start_sf_start, ts_start_sf_stop) = self
416            .find_slice_indices_of_templated_pos(template_slice.start, None, None)
417            .ok_or("Position not found in templated file")?;
418
419        let ts_start_subsliced_file = &sliced_file[ts_start_sf_start..ts_start_sf_stop];
420
421        // Work out the insertion point
422        let mut insertion_point: isize = -1;
423        for elem in ts_start_subsliced_file.iter() {
424            // Do slice starts and ends
425            for &slice_elem in ["start", "stop"].iter() {
426                let elem_val = match slice_elem {
427                    "start" => elem.templated_slice.start,
428                    "stop" => elem.templated_slice.end,
429                    _ => panic!("Unexpected slice_elem"),
430                };
431
432                if elem_val == template_slice.start {
433                    let point = if slice_elem == "start" {
434                        elem.source_slice.start
435                    } else {
436                        elem.source_slice.end
437                    };
438
439                    let point: isize = point.try_into().unwrap();
440                    if insertion_point < 0 || point < insertion_point {
441                        insertion_point = point;
442                    }
443                    // We don't break here, because we might find ANOTHER
444                    // later which is actually earlier.
445                }
446            }
447        }
448
449        // Zero length slice.
450        if template_slice.start == template_slice.end {
451            // Is it on a join?
452            return if insertion_point >= 0 {
453                Ok(zero_slice(insertion_point.try_into().unwrap()))
454                // It's within a segment.
455            } else if !ts_start_subsliced_file.is_empty()
456                && ts_start_subsliced_file[0].has_slice_kind(TemplateSliceKind::Literal)
457            {
458                let offset =
459                    template_slice.start - ts_start_subsliced_file[0].templated_slice.start;
460                Ok(zero_slice(
461                    ts_start_subsliced_file[0].source_slice.start + offset,
462                ))
463            } else {
464                Err(format!(
465                    "Attempting a single length slice within a templated section! {template_slice:?} within \
466                     {ts_start_subsliced_file:?}."
467                ))
468            };
469        }
470
471        let (ts_stop_sf_start, ts_stop_sf_stop) = self
472            .find_slice_indices_of_templated_pos(template_slice.end, None, Some(false))
473            .ok_or("Position not found in templated file")?;
474
475        let mut ts_start_sf_start = ts_start_sf_start;
476        if insertion_point >= 0 {
477            for elem in &sliced_file[ts_start_sf_start..] {
478                let insertion_point: usize = insertion_point.try_into().unwrap();
479                if elem.source_slice.start != insertion_point {
480                    ts_start_sf_start += 1;
481                } else {
482                    break;
483                }
484            }
485        }
486
487        let subslices = &sliced_file[usize::min(ts_start_sf_start, ts_stop_sf_start)
488            ..usize::max(ts_start_sf_stop, ts_stop_sf_stop)];
489
490        let start_slices = if ts_start_sf_start == ts_start_sf_stop {
491            return match ts_start_sf_start.cmp(&sliced_file.len()) {
492                Ordering::Greater => {
493                    panic!("Starting position higher than sliced file position")
494                }
495                Ordering::Less => Ok(sliced_file[1].source_slice.clone()),
496                Ordering::Equal => Ok(sliced_file.last().unwrap().source_slice.clone()),
497            };
498        } else {
499            &sliced_file[ts_start_sf_start..ts_start_sf_stop]
500        };
501
502        let stop_slices = if ts_stop_sf_start == ts_stop_sf_stop {
503            vec![sliced_file[ts_stop_sf_start].clone()]
504        } else {
505            sliced_file[ts_stop_sf_start..ts_stop_sf_stop].to_vec()
506        };
507
508        let source_start: isize = if insertion_point >= 0 {
509            insertion_point
510        } else if start_slices[0].has_slice_kind(TemplateSliceKind::Literal) {
511            let offset = template_slice.start - start_slices[0].templated_slice.start;
512            (start_slices[0].source_slice.start + offset)
513                .try_into()
514                .unwrap()
515        } else {
516            start_slices[0].source_slice.start.try_into().unwrap()
517        };
518
519        let source_stop = if stop_slices
520            .last()
521            .unwrap()
522            .has_slice_kind(TemplateSliceKind::Literal)
523        {
524            let offset = stop_slices.last().unwrap().templated_slice.end - template_slice.end;
525            stop_slices.last().unwrap().source_slice.end - offset
526        } else {
527            stop_slices.last().unwrap().source_slice.end
528        };
529
530        let source_slice;
531        if source_start > source_stop.try_into().unwrap() {
532            let mut source_start = usize::MAX;
533            let mut source_stop = 0;
534            for elem in subslices {
535                source_start = usize::min(source_start, elem.source_slice.start);
536                source_stop = usize::max(source_stop, elem.source_slice.end);
537            }
538            source_slice = source_start..source_stop;
539        } else {
540            source_slice = source_start.try_into().unwrap()..source_stop;
541        }
542
543        Ok(source_slice)
544    }
545
546    ///  Work out whether a slice of the source file is a literal or not.
547    pub fn is_source_slice_literal(&self, source_slice: &Range<usize>) -> bool {
548        // No sliced file? Everything is literal
549        if self.raw_sliced.is_empty() {
550            return true;
551        };
552
553        // Zero length slice. It's a literal, because it's definitely not templated.
554        if source_slice.start == source_slice.end {
555            return true;
556        };
557
558        let mut is_literal = true;
559        for raw_slice in &self.raw_sliced {
560            // Reset if we find a literal and we're up to the start
561            // otherwise set false.
562            if raw_slice.source_idx <= source_slice.start {
563                is_literal = raw_slice.has_slice_kind(TemplateSliceKind::Literal);
564            } else if raw_slice.source_idx >= source_slice.end {
565                break;
566            } else if !raw_slice.has_slice_kind(TemplateSliceKind::Literal) {
567                is_literal = false;
568            };
569        }
570        is_literal
571    }
572
573    /// Return a list of the raw slices spanning a set of indices.
574    pub fn raw_slices_spanning_source_slice(
575        &self,
576        source_slice: &Range<usize>,
577    ) -> Vec<RawFileSlice> {
578        // Special case: The source_slice is at the end of the file.
579        let last_raw_slice = self.raw_sliced.last().unwrap();
580        if source_slice.start >= last_raw_slice.source_idx + last_raw_slice.raw.len() {
581            return Vec::new();
582        }
583
584        // First find the start index
585        let mut raw_slice_idx = 0;
586        // Move the raw pointer forward to the start of this patch
587        while raw_slice_idx + 1 < self.raw_sliced.len()
588            && self.raw_sliced[raw_slice_idx + 1].source_idx <= source_slice.start
589        {
590            raw_slice_idx += 1;
591        }
592
593        // Find slice index of the end of this patch.
594        let mut slice_span = 1;
595        while raw_slice_idx + slice_span < self.raw_sliced.len()
596            && self.raw_sliced[raw_slice_idx + slice_span].source_idx < source_slice.end
597        {
598            slice_span += 1;
599        }
600
601        // Return the raw slices
602        self.raw_sliced[raw_slice_idx..(raw_slice_idx + slice_span)].to_vec()
603    }
604}
605
606/// Find the indices of all newlines in a string.
607pub fn iter_indices_of_newlines(raw_str: &str) -> impl Iterator<Item = usize> + '_ {
608    // TODO: This may be optimize-able by not doing it all up front.
609    raw_str.match_indices('\n').map(|(idx, _)| idx)
610}
611
612#[cfg_attr(feature = "stringify", derive(Serialize, Deserialize))]
613#[derive(Debug, PartialEq, Eq, Clone, Hash)]
614pub enum RawFileSliceType {
615    Comment,
616    BlockEnd,
617    BlockStart,
618    BlockMid,
619}
620
621/// A slice referring to a raw file.
622#[cfg_attr(feature = "stringify", derive(Serialize, Deserialize))]
623#[derive(Debug, PartialEq, Eq, Clone, Hash)]
624pub struct RawFileSlice {
625    /// Source string
626    raw: String,
627    pub slice_type: TemplateSliceKind,
628    /// Offset from beginning of source string
629    pub source_idx: usize,
630    slice_subtype: Option<RawFileSliceType>,
631    /// Block index, incremented on start or end block tags, e.g. "if", "for"
632    block_idx: usize,
633}
634
635impl RawFileSlice {
636    pub fn new(
637        raw: String,
638        slice_type: TemplateSliceKind,
639        source_idx: usize,
640        slice_subtype: Option<RawFileSliceType>,
641        block_idx: Option<usize>,
642    ) -> Self {
643        Self {
644            raw,
645            slice_type,
646            source_idx,
647            slice_subtype,
648            block_idx: block_idx.unwrap_or(0),
649        }
650    }
651
652    pub fn new_typed(
653        raw: String,
654        slice_type: TemplateSliceKind,
655        source_idx: usize,
656        slice_subtype: Option<RawFileSliceType>,
657        block_idx: Option<usize>,
658    ) -> Self {
659        Self::new(raw, slice_type, source_idx, slice_subtype, block_idx)
660    }
661}
662
663impl RawFileSlice {
664    /// Return the closing index of this slice.
665    fn end_source_idx(&self) -> usize {
666        self.source_idx + self.raw.len()
667    }
668
669    /// Return the a slice object for this slice.
670    pub fn source_slice(&self) -> Range<usize> {
671        self.source_idx..self.end_source_idx()
672    }
673
674    /// Return the raw source string for this slice.
675    pub fn raw(&self) -> &str {
676        &self.raw
677    }
678
679    /// Return the slice type (e.g., literal, templated, comment).
680    pub const fn slice_type(&self) -> TemplateSliceKind {
681        self.slice_type
682    }
683
684    pub const fn slice_kind(&self) -> TemplateSliceKind {
685        self.slice_type
686    }
687
688    pub fn has_slice_kind(&self, kind: TemplateSliceKind) -> bool {
689        self.slice_kind() == kind
690    }
691
692    /// Based on its slice_type, does it only appear in the *source*?
693    /// There are some slice types which are automatically source only.
694    /// There are *also* some which are source only because they render
695    /// to an empty string.
696    fn is_source_only_slice(&self) -> bool {
697        self.slice_kind().is_source_only()
698    }
699}
700
701/// Build a mapping from character (Unicode code point) indices to byte indices.
702///
703/// Python uses character-based indices, while Rust's `String::len()` returns
704/// byte length (UTF-8). This function creates a lookup table to convert between
705/// the two coordinate systems.
706///
707/// The returned vector has length `num_chars + 1`, where entry `i` gives the
708/// byte offset of the `i`-th character, and the last entry is the total byte
709/// length (for end-of-string conversions).
710pub fn char_to_byte_indices(s: &str) -> Vec<usize> {
711    let mut indices: Vec<usize> = s.char_indices().map(|(byte_idx, _)| byte_idx).collect();
712    indices.push(s.len());
713    indices
714}
715
716/// Convert a character-based index to a byte-based index using a precomputed
717/// mapping table from [`char_to_byte_indices`].
718///
719/// # Panics
720///
721/// Panics if `char_idx` is greater than or equal to `char_to_byte.len()`.
722/// This indicates a bug in the caller (e.g. using an index that is not
723/// derived from the same string used to build `char_to_byte`).
724pub fn char_idx_to_byte_idx(char_to_byte: &[usize], char_idx: usize) -> usize {
725    assert!(
726        char_idx < char_to_byte.len(),
727        "char_idx_to_byte_idx: char_idx {char_idx} out of bounds for mapping of length {}",
728        char_to_byte.len()
729    );
730    char_to_byte[char_idx]
731}
732
733#[cfg(test)]
734mod tests {
735    use super::*;
736
737    #[test]
738    fn test_char_to_byte_indices_ascii() {
739        let indices = char_to_byte_indices("hello");
740        assert_eq!(indices, vec![0, 1, 2, 3, 4, 5]);
741    }
742
743    #[test]
744    fn test_char_to_byte_indices_multibyte() {
745        // "あいう" = 3 chars, 9 bytes (each Japanese char is 3 bytes in UTF-8)
746        let indices = char_to_byte_indices("あいう");
747        assert_eq!(indices, vec![0, 3, 6, 9]);
748    }
749
750    #[test]
751    fn test_char_to_byte_indices_mixed() {
752        // "aあb" = 3 chars; 'a'=1byte, 'あ'=3bytes, 'b'=1byte => total 5 bytes
753        let indices = char_to_byte_indices("aあb");
754        assert_eq!(indices, vec![0, 1, 4, 5]);
755    }
756
757    #[test]
758    fn test_char_to_byte_indices_accented() {
759        // "café" = 4 chars; 'c'=1, 'a'=1, 'f'=1, 'é'=2 => total 5 bytes
760        let indices = char_to_byte_indices("café");
761        assert_eq!(indices, vec![0, 1, 2, 3, 5]);
762    }
763
764    #[test]
765    fn test_char_to_byte_indices_empty() {
766        let indices = char_to_byte_indices("");
767        assert_eq!(indices, vec![0]);
768    }
769
770    #[test]
771    fn test_char_idx_to_byte_idx_conversion() {
772        let indices = char_to_byte_indices("aあb");
773        assert_eq!(char_idx_to_byte_idx(&indices, 0), 0);
774        assert_eq!(char_idx_to_byte_idx(&indices, 1), 1);
775        assert_eq!(char_idx_to_byte_idx(&indices, 2), 4);
776        assert_eq!(char_idx_to_byte_idx(&indices, 3), 5);
777    }
778
779    #[test]
780    fn test_templated_file_multibyte_consistency_check() {
781        // Regression test: TemplatedFile::new should not panic when source
782        // contains multi-byte UTF-8 characters, as long as indices are
783        // byte-based. This simulates the scenario after Python char indices
784        // have been converted to Rust byte indices.
785        //
786        // Source: "-- 日本語\nSELECT 1"
787        //   "-- 日本語" = 12 bytes (2+1+3+3+3 = '--'+' '+'日'+'本'+'語')
788        //   "\n" = 1 byte
789        //   "SELECT 1" = 8 bytes
790        //   Total: 21 bytes
791        let source = "-- 日本語\nSELECT 1".to_string();
792        assert_eq!(source.len(), 21);
793
794        let raw_sliced = vec![RawFileSlice::new(
795            source.clone(),
796            TemplateSliceKind::Literal,
797            0,
798            None,
799            None,
800        )];
801        let sliced_file = vec![TemplatedFileSlice::new(
802            TemplateSliceKind::Literal,
803            0..source.len(),
804            0..source.len(),
805        )];
806
807        // This must not panic
808        let tf = TemplatedFile::new(
809            source.clone(),
810            "test.sql".to_string(),
811            Some(source.clone()),
812            Some(sliced_file),
813            Some(raw_sliced),
814        )
815        .unwrap();
816        assert_eq!(tf.source_str, source);
817    }
818
819    #[test]
820    fn test_templated_file_multibyte_multiple_raw_slices() {
821        // Simulates a templated file with multi-byte characters split across
822        // multiple raw slices, using byte-based indices (post-conversion).
823        //
824        // Source: "SELECT 'café'" = 14 bytes ('é' is 2 bytes)
825        // Split into: "SELECT '" (8 bytes) + "café" (5 bytes) + "'" (1 byte)
826        let source = "SELECT 'café'".to_string();
827        assert_eq!(source.len(), 14);
828
829        let raw_sliced = vec![
830            RawFileSlice::new(
831                "SELECT '".to_string(),
832                TemplateSliceKind::Literal,
833                0,
834                None,
835                None,
836            ),
837            RawFileSlice::new(
838                "café".to_string(),
839                TemplateSliceKind::Templated,
840                8, // byte offset
841                None,
842                None,
843            ),
844            RawFileSlice::new(
845                "'".to_string(),
846                TemplateSliceKind::Literal,
847                13, // byte offset (8 + 5)
848                None,
849                None,
850            ),
851        ];
852        let sliced_file = vec![
853            TemplatedFileSlice::new(TemplateSliceKind::Literal, 0..8, 0..8),
854            TemplatedFileSlice::new(TemplateSliceKind::Templated, 8..13, 8..13),
855            TemplatedFileSlice::new(TemplateSliceKind::Literal, 13..14, 13..14),
856        ];
857
858        let tf = TemplatedFile::new(
859            source.clone(),
860            "test.sql".to_string(),
861            Some(source.clone()),
862            Some(sliced_file),
863            Some(raw_sliced),
864        )
865        .unwrap();
866        assert_eq!(tf.source_str, source);
867    }
868
869    #[test]
870    #[should_panic(expected = "Consistency fail on running source length")]
871    fn test_templated_file_char_indices_cause_panic() {
872        // Demonstrates that using Python's character-based indices (without
873        // conversion) causes a panic. This is the bug scenario.
874        //
875        // Source: "aあb" = 3 chars in Python, 5 bytes in Rust
876        // If we use char indices (0, 2) instead of byte indices (0, 4) for
877        // the second slice, the consistency check fails.
878        let source = "aあb".to_string();
879
880        let raw_sliced = vec![
881            RawFileSlice::new(
882                "aあ".to_string(), // 4 bytes
883                TemplateSliceKind::Literal,
884                0,
885                None,
886                None,
887            ),
888            RawFileSlice::new(
889                "b".to_string(),
890                TemplateSliceKind::Literal,
891                2, // WRONG: char index from Python (should be 4 for bytes)
892                None,
893                None,
894            ),
895        ];
896        let sliced_file = vec![
897            TemplatedFileSlice::new(TemplateSliceKind::Literal, 0..2, 0..2),
898            TemplatedFileSlice::new(TemplateSliceKind::Literal, 2..3, 2..3),
899        ];
900
901        // This SHOULD panic because source_idx=2 != pos=4
902        let _ = TemplatedFile::new(
903            source,
904            "test.sql".to_string(),
905            Some("aあb".to_string()),
906            Some(sliced_file),
907            Some(raw_sliced),
908        );
909    }
910
911    #[test]
912    fn test_indices_of_newlines() {
913        vec![
914            ("", vec![]),
915            ("foo", vec![]),
916            ("foo\nbar", vec![3]),
917            ("\nfoo\n\nbar\nfoo\n\nbar\n", vec![0, 4, 5, 9, 13, 14, 18]),
918        ]
919        .into_iter()
920        .for_each(|(in_str, expected)| {
921            assert_eq!(
922                expected,
923                iter_indices_of_newlines(in_str).collect::<Vec<usize>>()
924            )
925        });
926    }
927
928    // const SIMPLE_SOURCE_STR: &str = "01234\n6789{{foo}}fo\nbarss";
929    // const SIMPLE_TEMPLATED_STR: &str = "01234\n6789x\nfo\nbarfss";
930
931    fn simple_sliced_file() -> Vec<TemplatedFileSlice> {
932        vec![
933            TemplatedFileSlice::new(TemplateSliceKind::Literal, 0..10, 0..10),
934            TemplatedFileSlice::new(TemplateSliceKind::Templated, 10..17, 10..12),
935            TemplatedFileSlice::new(TemplateSliceKind::Literal, 17..25, 12..20),
936        ]
937    }
938
939    fn simple_raw_sliced_file() -> [RawFileSlice; 3] {
940        [
941            RawFileSlice::new("x".repeat(10), TemplateSliceKind::Literal, 0, None, None),
942            RawFileSlice::new("x".repeat(7), TemplateSliceKind::Templated, 10, None, None),
943            RawFileSlice::new("x".repeat(8), TemplateSliceKind::Literal, 17, None, None),
944        ]
945    }
946
947    fn complex_sliced_file() -> Vec<TemplatedFileSlice> {
948        vec![
949            TemplatedFileSlice::new(TemplateSliceKind::Literal, 0..13, 0..13),
950            TemplatedFileSlice::new(TemplateSliceKind::Comment, 13..29, 13..13),
951            TemplatedFileSlice::new(TemplateSliceKind::Literal, 29..44, 13..28),
952            TemplatedFileSlice::new(TemplateSliceKind::BlockStart, 44..68, 28..28),
953            TemplatedFileSlice::new(TemplateSliceKind::Literal, 68..81, 28..41),
954            TemplatedFileSlice::new(TemplateSliceKind::Templated, 81..86, 41..42),
955            TemplatedFileSlice::new(TemplateSliceKind::Literal, 86..110, 42..66),
956            TemplatedFileSlice::new(TemplateSliceKind::Templated, 68..86, 66..76),
957            TemplatedFileSlice::new(TemplateSliceKind::Literal, 68..81, 76..89),
958            TemplatedFileSlice::new(TemplateSliceKind::Templated, 81..86, 89..90),
959            TemplatedFileSlice::new(TemplateSliceKind::Literal, 86..110, 90..114),
960            TemplatedFileSlice::new(TemplateSliceKind::Templated, 68..86, 114..125),
961            TemplatedFileSlice::new(TemplateSliceKind::Literal, 68..81, 125..138),
962            TemplatedFileSlice::new(TemplateSliceKind::Templated, 81..86, 138..139),
963            TemplatedFileSlice::new(TemplateSliceKind::Literal, 86..110, 139..163),
964            TemplatedFileSlice::new(TemplateSliceKind::Templated, 110..123, 163..166),
965            TemplatedFileSlice::new(TemplateSliceKind::Literal, 123..132, 166..175),
966            TemplatedFileSlice::new(TemplateSliceKind::BlockEnd, 132..144, 175..175),
967            TemplatedFileSlice::new(TemplateSliceKind::Literal, 144..155, 175..186),
968            TemplatedFileSlice::new(TemplateSliceKind::BlockStart, 155..179, 186..186),
969            TemplatedFileSlice::new(TemplateSliceKind::Literal, 179..189, 186..196),
970            TemplatedFileSlice::new(TemplateSliceKind::Templated, 189..194, 196..197),
971            TemplatedFileSlice::new(TemplateSliceKind::Literal, 194..203, 197..206),
972            TemplatedFileSlice::new(TemplateSliceKind::Literal, 179..189, 206..216),
973            TemplatedFileSlice::new(TemplateSliceKind::Templated, 189..194, 216..217),
974            TemplatedFileSlice::new(TemplateSliceKind::Literal, 194..203, 217..226),
975            TemplatedFileSlice::new(TemplateSliceKind::Literal, 179..189, 226..236),
976            TemplatedFileSlice::new(TemplateSliceKind::Templated, 189..194, 236..237),
977            TemplatedFileSlice::new(TemplateSliceKind::Literal, 194..203, 237..246),
978            TemplatedFileSlice::new(TemplateSliceKind::BlockEnd, 203..215, 246..246),
979            TemplatedFileSlice::new(TemplateSliceKind::Literal, 215..230, 246..261),
980        ]
981    }
982
983    fn complex_raw_sliced_file() -> Vec<RawFileSlice> {
984        vec![
985            RawFileSlice::new(
986                "x".repeat(13).to_string(),
987                TemplateSliceKind::Literal,
988                0,
989                None,
990                None,
991            ),
992            RawFileSlice::new(
993                "x".repeat(16).to_string(),
994                TemplateSliceKind::Comment,
995                13,
996                None,
997                None,
998            ),
999            RawFileSlice::new(
1000                "x".repeat(15).to_string(),
1001                TemplateSliceKind::Literal,
1002                29,
1003                None,
1004                None,
1005            ),
1006            RawFileSlice::new(
1007                "x".repeat(24).to_string(),
1008                TemplateSliceKind::BlockStart,
1009                44,
1010                None,
1011                None,
1012            ),
1013            RawFileSlice::new(
1014                "x".repeat(13).to_string(),
1015                TemplateSliceKind::Literal,
1016                68,
1017                None,
1018                None,
1019            ),
1020            RawFileSlice::new(
1021                "x".repeat(5).to_string(),
1022                TemplateSliceKind::Templated,
1023                81,
1024                None,
1025                None,
1026            ),
1027            RawFileSlice::new(
1028                "x".repeat(24).to_string(),
1029                TemplateSliceKind::Literal,
1030                86,
1031                None,
1032                None,
1033            ),
1034            RawFileSlice::new(
1035                "x".repeat(13).to_string(),
1036                TemplateSliceKind::Templated,
1037                110,
1038                None,
1039                None,
1040            ),
1041            RawFileSlice::new(
1042                "x".repeat(9).to_string(),
1043                TemplateSliceKind::Literal,
1044                123,
1045                None,
1046                None,
1047            ),
1048            RawFileSlice::new(
1049                "x".repeat(12).to_string(),
1050                TemplateSliceKind::BlockEnd,
1051                132,
1052                None,
1053                None,
1054            ),
1055            RawFileSlice::new(
1056                "x".repeat(11).to_string(),
1057                TemplateSliceKind::Literal,
1058                144,
1059                None,
1060                None,
1061            ),
1062            RawFileSlice::new(
1063                "x".repeat(24).to_string(),
1064                TemplateSliceKind::BlockStart,
1065                155,
1066                None,
1067                None,
1068            ),
1069            RawFileSlice::new(
1070                "x".repeat(10).to_string(),
1071                TemplateSliceKind::Literal,
1072                179,
1073                None,
1074                None,
1075            ),
1076            RawFileSlice::new(
1077                "x".repeat(5).to_string(),
1078                TemplateSliceKind::Templated,
1079                189,
1080                None,
1081                None,
1082            ),
1083            RawFileSlice::new(
1084                "x".repeat(9).to_string(),
1085                TemplateSliceKind::Literal,
1086                194,
1087                None,
1088                None,
1089            ),
1090            RawFileSlice::new(
1091                "x".repeat(12).to_string(),
1092                TemplateSliceKind::BlockEnd,
1093                203,
1094                None,
1095                None,
1096            ),
1097            RawFileSlice::new(
1098                "x".repeat(15).to_string(),
1099                TemplateSliceKind::Literal,
1100                215,
1101                None,
1102                None,
1103            ),
1104        ]
1105    }
1106
1107    struct FileKwargs {
1108        f_name: String,
1109        source_str: String,
1110        templated_str: Option<String>,
1111        sliced_file: Vec<TemplatedFileSlice>,
1112        raw_sliced_file: Vec<RawFileSlice>,
1113    }
1114
1115    fn simple_file_kwargs() -> FileKwargs {
1116        FileKwargs {
1117            f_name: "test.sql".to_string(),
1118            source_str: "01234\n6789{{foo}}fo\nbarss".to_string(),
1119            templated_str: Some("01234\n6789x\nfo\nbarss".to_string()),
1120            sliced_file: simple_sliced_file().to_vec(),
1121            raw_sliced_file: simple_raw_sliced_file().to_vec(),
1122        }
1123    }
1124
1125    fn complex_file_kwargs() -> FileKwargs {
1126        FileKwargs {
1127            f_name: "test.sql".to_string(),
1128            source_str: complex_raw_sliced_file()
1129                .iter()
1130                .fold(String::new(), |acc, x| acc + &x.raw),
1131            templated_str: None,
1132            sliced_file: complex_sliced_file().to_vec(),
1133            raw_sliced_file: complex_raw_sliced_file().to_vec(),
1134        }
1135    }
1136
1137    #[test]
1138    /// Test TemplatedFile.get_line_pos_of_char_pos.
1139    fn test_templated_file_get_line_pos_of_char_pos() {
1140        let tests = [
1141            (simple_file_kwargs(), 0, 1, 1),
1142            (simple_file_kwargs(), 20, 3, 1),
1143            (simple_file_kwargs(), 24, 3, 5),
1144        ];
1145
1146        for test in tests {
1147            let kwargs = test.0;
1148
1149            let tf = TemplatedFile::new(
1150                kwargs.source_str,
1151                kwargs.f_name,
1152                kwargs.templated_str,
1153                Some(kwargs.sliced_file),
1154                Some(kwargs.raw_sliced_file),
1155            )
1156            .unwrap();
1157
1158            let (res_line_no, res_line_pos) = tf.get_line_pos_of_char_pos(test.1, true);
1159
1160            assert_eq!(res_line_no, test.2);
1161            assert_eq!(res_line_pos, test.3);
1162        }
1163    }
1164
1165    #[test]
1166    fn test_templated_file_find_slice_indices_of_templated_pos() {
1167        let tests = vec![
1168            // "templated_position,inclusive,file_slices,sliced_idx_start,sliced_idx_stop",
1169            // TODO Fix these
1170            // (100, true, complex_file_kwargs(), 10, 11),
1171            // (13, true, complex_file_kwargs(), 0, 3),
1172            // (28, true, complex_file_kwargs(), 2, 5),
1173            // # Check end slicing.
1174            (12, true, simple_file_kwargs(), 1, 3),
1175            (20, true, simple_file_kwargs(), 2, 3),
1176            // Check inclusivity
1177            // (13, false, complex_file_kwargs(), 0, 1),
1178        ];
1179
1180        for test in tests {
1181            let args = test.2;
1182
1183            let file = TemplatedFile::new(
1184                args.source_str,
1185                args.f_name,
1186                args.templated_str,
1187                Some(args.sliced_file),
1188                Some(args.raw_sliced_file),
1189            )
1190            .unwrap();
1191
1192            let (res_start, res_stop) = file
1193                .find_slice_indices_of_templated_pos(test.0, None, Some(test.1))
1194                .unwrap();
1195
1196            assert_eq!(res_start, test.3);
1197            assert_eq!(res_stop, test.4);
1198        }
1199    }
1200
1201    #[test]
1202    /// Test TemplatedFile.templated_slice_to_source_slice
1203    fn test_templated_file_templated_slice_to_source_slice() {
1204        let test_cases = vec![
1205            // Simple example
1206            (
1207                5..10,
1208                5..10,
1209                true,
1210                FileKwargs {
1211                    sliced_file: vec![TemplatedFileSlice::new(
1212                        TemplateSliceKind::Literal,
1213                        0..20,
1214                        0..20,
1215                    )],
1216                    raw_sliced_file: vec![RawFileSlice::new(
1217                        "x".repeat(20),
1218                        TemplateSliceKind::Literal,
1219                        0,
1220                        None,
1221                        None,
1222                    )],
1223                    source_str: "x".repeat(20),
1224                    f_name: "foo.sql".to_string(),
1225                    templated_str: None,
1226                },
1227            ),
1228            // Trimming the end of a literal (with things that follow).
1229            (10..13, 10..13, true, complex_file_kwargs()),
1230            // // Unrealistic, but should still work
1231            (
1232                5..10,
1233                55..60,
1234                true,
1235                FileKwargs {
1236                    sliced_file: vec![TemplatedFileSlice::new(
1237                        TemplateSliceKind::Literal,
1238                        50..70,
1239                        0..20,
1240                    )],
1241                    raw_sliced_file: vec![
1242                        RawFileSlice::new(
1243                            "x".repeat(50),
1244                            TemplateSliceKind::Literal,
1245                            0,
1246                            None,
1247                            None,
1248                        ),
1249                        RawFileSlice::new(
1250                            "x".repeat(20),
1251                            TemplateSliceKind::Literal,
1252                            50,
1253                            None,
1254                            None,
1255                        ),
1256                    ],
1257                    source_str: "x".repeat(70),
1258                    f_name: "foo.sql".to_string(),
1259                    templated_str: None,
1260                },
1261            ),
1262            // // Spanning a template
1263            (5..15, 5..20, false, simple_file_kwargs()),
1264            // // Handling templated
1265            (
1266                5..15,
1267                0..25,
1268                false,
1269                FileKwargs {
1270                    sliced_file: simple_file_kwargs()
1271                        .sliced_file
1272                        .iter()
1273                        .map(|slc| {
1274                            TemplatedFileSlice::new(
1275                                TemplateSliceKind::Templated,
1276                                slc.source_slice.clone(),
1277                                slc.templated_slice.clone(),
1278                            )
1279                        })
1280                        .collect(),
1281                    raw_sliced_file: simple_file_kwargs()
1282                        .raw_sliced_file
1283                        .iter()
1284                        .map(|slc| {
1285                            RawFileSlice::new(
1286                                slc.raw.to_string(),
1287                                TemplateSliceKind::Templated,
1288                                slc.source_idx,
1289                                None,
1290                                None,
1291                            )
1292                        })
1293                        .collect(),
1294                    ..simple_file_kwargs()
1295                },
1296            ),
1297            // // Handling single length slices
1298            (10..10, 10..10, true, simple_file_kwargs()),
1299            (12..12, 17..17, true, simple_file_kwargs()),
1300            // // Dealing with single length elements
1301            (
1302                20..20,
1303                25..25,
1304                true,
1305                FileKwargs {
1306                    sliced_file: simple_file_kwargs()
1307                        .sliced_file
1308                        .into_iter()
1309                        .chain(vec![TemplatedFileSlice::new(
1310                            TemplateSliceKind::Comment,
1311                            25..35,
1312                            20..20,
1313                        )])
1314                        .collect(),
1315                    raw_sliced_file: simple_file_kwargs()
1316                        .raw_sliced_file
1317                        .into_iter()
1318                        .chain(vec![RawFileSlice::new(
1319                            "x".repeat(10),
1320                            TemplateSliceKind::Comment,
1321                            25,
1322                            None,
1323                            None,
1324                        )])
1325                        .collect(),
1326                    source_str: simple_file_kwargs().source_str.to_string() + &"x".repeat(10),
1327                    ..simple_file_kwargs()
1328                },
1329            ),
1330            // // Just more test coverage
1331            (43..43, 87..87, true, complex_file_kwargs()),
1332            (13..13, 13..13, true, complex_file_kwargs()),
1333            (186..186, 155..155, true, complex_file_kwargs()),
1334            // Backward slicing.
1335            (
1336                100..130,
1337                // NB This actually would reference the wrong way around if we
1338                // just take the points. Here we should handle it gracefully.
1339                68..110,
1340                false,
1341                complex_file_kwargs(),
1342            ),
1343        ];
1344
1345        for (in_slice, out_slice, is_literal, tf_kwargs) in test_cases {
1346            let file = TemplatedFile::new(
1347                tf_kwargs.source_str,
1348                tf_kwargs.f_name,
1349                tf_kwargs.templated_str,
1350                Some(tf_kwargs.sliced_file),
1351                Some(tf_kwargs.raw_sliced_file),
1352            )
1353            .unwrap();
1354
1355            let source_slice = file.templated_slice_to_source_slice(in_slice).unwrap();
1356            let literal_test = file.is_source_slice_literal(&source_slice);
1357
1358            assert_eq!((is_literal, source_slice), (literal_test, out_slice));
1359        }
1360    }
1361
1362    #[test]
1363    /// Test TemplatedFile.source_only_slices
1364    fn test_templated_file_source_only_slices() {
1365        let test_cases = vec![
1366            // Comment example
1367            (
1368                TemplatedFile::new(
1369                    format!("{}{}{}", "a".repeat(10), "{# b #}", "a".repeat(10)),
1370                    "test".to_string(),
1371                    None,
1372                    Some(vec![
1373                        TemplatedFileSlice::new(TemplateSliceKind::Literal, 0..10, 0..10),
1374                        TemplatedFileSlice::new(TemplateSliceKind::Templated, 10..17, 10..10),
1375                        TemplatedFileSlice::new(TemplateSliceKind::Literal, 17..27, 10..20),
1376                    ]),
1377                    Some(vec![
1378                        RawFileSlice::new(
1379                            "a".repeat(10).to_string(),
1380                            TemplateSliceKind::Literal,
1381                            0,
1382                            None,
1383                            None,
1384                        ),
1385                        RawFileSlice::new(
1386                            "{# b #}".to_string(),
1387                            TemplateSliceKind::Comment,
1388                            10,
1389                            None,
1390                            None,
1391                        ),
1392                        RawFileSlice::new(
1393                            "a".repeat(10).to_string(),
1394                            TemplateSliceKind::Literal,
1395                            17,
1396                            None,
1397                            None,
1398                        ),
1399                    ]),
1400                )
1401                .unwrap(),
1402                vec![RawFileSlice::new(
1403                    "{# b #}".to_string(),
1404                    TemplateSliceKind::Comment,
1405                    10,
1406                    None,
1407                    None,
1408                )],
1409            ),
1410            // Template tags aren't source only.
1411            (
1412                TemplatedFile::new(
1413                    "aaa{{ b }}aaa".to_string(),
1414                    "test".to_string(),
1415                    None,
1416                    Some(vec![
1417                        TemplatedFileSlice::new(TemplateSliceKind::Literal, 0..3, 0..3),
1418                        TemplatedFileSlice::new(TemplateSliceKind::Templated, 3..10, 3..6),
1419                        TemplatedFileSlice::new(TemplateSliceKind::Literal, 10..13, 6..9),
1420                    ]),
1421                    Some(vec![
1422                        RawFileSlice::new(
1423                            "aaa".to_string(),
1424                            TemplateSliceKind::Literal,
1425                            0,
1426                            None,
1427                            None,
1428                        ),
1429                        RawFileSlice::new(
1430                            "{{ b }}".to_string(),
1431                            TemplateSliceKind::Templated,
1432                            3,
1433                            None,
1434                            None,
1435                        ),
1436                        RawFileSlice::new(
1437                            "aaa".to_string(),
1438                            TemplateSliceKind::Literal,
1439                            10,
1440                            None,
1441                            None,
1442                        ),
1443                    ]),
1444                )
1445                .unwrap(),
1446                vec![],
1447            ),
1448        ];
1449
1450        for (file, expected) in test_cases {
1451            assert_eq!(file.source_only_slices(), expected, "Failed for {:?}", file);
1452        }
1453    }
1454
1455    #[test]
1456    fn template_slice_kind_parses_legacy_strings() {
1457        assert_eq!(
1458            TemplateSliceKind::from_slice_type("block_start").unwrap(),
1459            TemplateSliceKind::BlockStart
1460        );
1461    }
1462
1463    #[test]
1464    fn raw_file_slice_source_only_uses_typed_adapter() {
1465        let comment = RawFileSlice::new_typed(
1466            "/* comment */".to_string(),
1467            TemplateSliceKind::Comment,
1468            0,
1469            None,
1470            None,
1471        );
1472        let literal = RawFileSlice::new_typed(
1473            "select".to_string(),
1474            TemplateSliceKind::Literal,
1475            0,
1476            None,
1477            None,
1478        );
1479
1480        assert!(comment.is_source_only_slice());
1481        assert!(!literal.is_source_only_slice());
1482    }
1483}