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