sqruff_lib/utils/reflow/
elements.rs

1use std::cell::OnceCell;
2use std::iter::zip;
3use std::ops::Deref;
4use std::rc::Rc;
5
6use itertools::{Itertools, chain};
7use nohash_hasher::IntMap;
8use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
9use sqruff_lib_core::lint_fix::LintFix;
10use sqruff_lib_core::parser::segments::{ErasedSegment, SegmentBuilder, Tables};
11
12use super::config::{ReflowConfig, Spacing};
13use super::depth_map::DepthInfo;
14use super::respace::determine_constraints;
15use crate::core::rules::LintResult;
16use crate::utils::reflow::rebreak::LinePosition;
17use crate::utils::reflow::respace::{
18    handle_respace_inline_with_space, handle_respace_inline_without_space, process_spacing,
19};
20
21fn get_consumed_whitespace(segment: Option<&ErasedSegment>) -> Option<String> {
22    let segment = segment?;
23
24    if segment.is_type(SyntaxKind::Placeholder) {
25        None
26    } else {
27        // match segment.block_type.as_ref() {
28        //     SyntaxKind::Literal => Some(segment.source_str),
29        //     _ => None,
30        // }
31        None
32    }
33}
34
35#[derive(Debug, Clone, Default, PartialEq)]
36pub struct ReflowPointData {
37    segments: Vec<ErasedSegment>,
38    stats: OnceCell<IndentStats>,
39    class_types: OnceCell<SyntaxSet>,
40}
41
42#[derive(Debug, Clone, Default, PartialEq)]
43pub struct ReflowPoint {
44    value: Rc<ReflowPointData>,
45}
46
47impl Deref for ReflowPoint {
48    type Target = ReflowPointData;
49
50    fn deref(&self) -> &Self::Target {
51        self.value.as_ref()
52    }
53}
54
55impl ReflowPoint {
56    pub fn new(segments: Vec<ErasedSegment>) -> Self {
57        Self {
58            value: Rc::new(ReflowPointData {
59                segments,
60                stats: OnceCell::new(),
61                class_types: OnceCell::new(),
62            }),
63        }
64    }
65
66    pub fn raw(&self) -> String {
67        self.segments.iter().map(|it| it.raw()).join("")
68    }
69
70    pub fn class_types(&self) -> &SyntaxSet {
71        self.class_types.get_or_init(|| {
72            self.segments
73                .iter()
74                .flat_map(|it| it.class_types())
75                .collect()
76        })
77    }
78
79    fn generate_indent_stats(segments: &[ErasedSegment]) -> IndentStats {
80        let mut trough = 0;
81        let mut running_sum = 0;
82        let mut implicit_indents = Vec::new();
83
84        for seg in segments {
85            if seg.is_indent() {
86                running_sum += seg.indent_val() as isize;
87
88                if seg.is_type(SyntaxKind::Implicit) {
89                    implicit_indents.push(running_sum);
90                }
91            }
92
93            if running_sum < trough {
94                trough = running_sum
95            }
96        }
97
98        IndentStats {
99            impulse: running_sum,
100            trough,
101            implicit_indents: implicit_indents.into(),
102        }
103    }
104
105    pub fn get_indent_segment(&self) -> Option<&ErasedSegment> {
106        let mut indent = None;
107        for seg in self.segments.iter().rev() {
108            if seg
109                .get_position_marker()
110                .filter(|pos_marker| !pos_marker.is_literal())
111                .is_some()
112            {
113                continue;
114            }
115
116            match seg.get_type() {
117                SyntaxKind::Newline => return indent,
118                SyntaxKind::Whitespace => {
119                    indent = Some(seg);
120                    continue;
121                }
122                _ => {}
123            }
124
125            if get_consumed_whitespace(Some(seg))
126                .unwrap_or_default()
127                .contains('\n')
128            {
129                return Some(seg);
130            }
131        }
132        indent
133    }
134
135    pub(crate) fn num_newlines(&self) -> usize {
136        self.segments
137            .iter()
138            .map(|seg| {
139                let newline_in_class = seg.class_types().contains(SyntaxKind::Newline) as usize;
140
141                let consumed_whitespace = get_consumed_whitespace(seg.into()).unwrap_or_default();
142                newline_in_class + consumed_whitespace.matches('\n').count()
143            })
144            .sum()
145    }
146
147    pub fn get_indent(&self) -> Option<String> {
148        if self.num_newlines() == 0 {
149            return None;
150        }
151
152        let seg = self.get_indent_segment();
153        let consumed_whitespace = get_consumed_whitespace(seg);
154
155        if let Some(consumed_whitespace) = consumed_whitespace {
156            return consumed_whitespace
157                .split('\n')
158                .next_back()
159                .unwrap()
160                .to_owned()
161                .into();
162        }
163
164        if let Some(seg) = seg {
165            Some(seg.raw().to_string())
166        } else {
167            String::new().into()
168        }
169    }
170
171    pub fn indent_to(
172        &self,
173        tables: &Tables,
174        desired_indent: &str,
175        after: Option<ErasedSegment>,
176        before: Option<ErasedSegment>,
177        description: Option<&str>,
178        source: Option<&str>,
179    ) -> (Vec<LintResult>, ReflowPoint) {
180        assert!(
181            !desired_indent.contains('\n'),
182            "Newline found in desired indent."
183        );
184        // Get the indent (or in the case of no newline, the last whitespace)
185        let indent_seg = self.get_indent_segment();
186
187        if indent_seg
188            .as_ref()
189            .filter(|indent_seg| indent_seg.is_type(SyntaxKind::Placeholder))
190            .is_some()
191        {
192            unimplemented!()
193        } else if self.num_newlines() != 0 {
194            if let Some(indent_seg) = indent_seg {
195                if indent_seg.raw() == desired_indent {
196                    unimplemented!()
197                } else if desired_indent.is_empty() {
198                    let idx = self
199                        .segments
200                        .iter()
201                        .position(|seg| seg == indent_seg)
202                        .unwrap();
203                    return (
204                        vec![LintResult::new(
205                            indent_seg.clone().into(),
206                            vec![LintFix::delete(indent_seg.clone())],
207                            Some(
208                                description
209                                    .map_or_else(
210                                        || "Line should not be indented.".to_owned(),
211                                        ToOwned::to_owned,
212                                    )
213                                    .to_string(),
214                            ),
215                            source.map(|s| s.to_string()),
216                        )],
217                        ReflowPoint::new(
218                            self.segments[..idx]
219                                .iter()
220                                .chain(self.segments[idx + 1..].iter())
221                                .cloned()
222                                .collect(),
223                        ),
224                    );
225                };
226
227                let new_indent =
228                    indent_seg.edit(tables.next_id(), desired_indent.to_owned().into(), None);
229                let idx = self
230                    .segments
231                    .iter()
232                    .position(|it| it == indent_seg)
233                    .unwrap();
234
235                let description = format!("Expected {}.", indent_description(desired_indent));
236
237                let lint_result = LintResult::new(
238                    indent_seg.clone().into(),
239                    vec![LintFix::replace(
240                        indent_seg.clone(),
241                        vec![new_indent.clone()],
242                        None,
243                    )],
244                    description.into(),
245                    None,
246                );
247
248                let mut new_segments = Vec::new();
249                new_segments.extend_from_slice(&self.segments[..idx]);
250                new_segments.push(new_indent);
251                new_segments.extend_from_slice(&self.segments[idx + 1..]);
252
253                let new_reflow_point = ReflowPoint::new(new_segments);
254
255                (vec![lint_result], new_reflow_point)
256            } else {
257                if desired_indent.is_empty() {
258                    return (Vec::new(), self.clone());
259                }
260
261                let new_indent = SegmentBuilder::whitespace(tables.next_id(), desired_indent);
262
263                let (last_newline_idx, last_newline) = self
264                    .segments
265                    .iter()
266                    .enumerate()
267                    .rev()
268                    .find(|(_, it)| {
269                        it.is_type(SyntaxKind::Newline)
270                            && it.get_position_marker().unwrap().is_literal()
271                    })
272                    .unwrap();
273
274                let mut new_segments = self.segments[..=last_newline_idx].to_vec();
275                new_segments.push(new_indent.clone());
276                new_segments.extend_from_slice(&self.segments[last_newline_idx + 1..]);
277
278                (
279                    vec![LintResult::new(
280                        if let Some(before) = before {
281                            before.into()
282                        } else {
283                            unimplemented!()
284                        },
285                        vec![LintFix::replace(
286                            last_newline.clone(),
287                            vec![last_newline.clone(), new_indent],
288                            None,
289                        )],
290                        format!("Expected {}", indent_description(desired_indent)).into(),
291                        None,
292                    )],
293                    ReflowPoint::new(new_segments),
294                )
295            }
296        } else {
297            // There isn't currently a newline.
298            let new_newline = SegmentBuilder::newline(tables.next_id(), "\n");
299            // Check for whitespace
300            let ws_seg = self
301                .segments
302                .iter()
303                .find(|seg| seg.is_type(SyntaxKind::Whitespace));
304
305            if let Some(ws_seg) = ws_seg {
306                let new_segs = if desired_indent.is_empty() {
307                    vec![new_newline]
308                } else {
309                    vec![
310                        new_newline,
311                        ws_seg.edit(tables.next_id(), desired_indent.to_owned().into(), None),
312                    ]
313                };
314                let idx = self.segments.iter().position(|it| ws_seg == it).unwrap();
315                let description = if let Some(before_seg) = before {
316                    format!(
317                        "Expected line break and {} before {:?}.",
318                        indent_description(desired_indent),
319                        before_seg.raw()
320                    )
321                } else if let Some(after_seg) = after {
322                    format!(
323                        "Expected line break and {} after {:?}.",
324                        indent_description(desired_indent),
325                        after_seg.raw()
326                    )
327                } else {
328                    format!(
329                        "Expected line break and {}.",
330                        indent_description(desired_indent)
331                    )
332                };
333
334                let fix = LintFix::replace(ws_seg.clone(), new_segs.clone(), None);
335                let new_point = ReflowPoint::new({
336                    let mut new_segments = Vec::new();
337
338                    // Add elements before the specified index
339                    if idx > 0 {
340                        new_segments.extend_from_slice(&self.segments[..idx]);
341                    }
342
343                    // Add new segments
344                    new_segments.extend(new_segs);
345
346                    // Add remaining elements after the specified index
347                    if idx < self.segments.len() {
348                        new_segments.extend_from_slice(&self.segments[idx + 1..]);
349                    }
350
351                    new_segments
352                });
353
354                (
355                    vec![LintResult::new(
356                        ws_seg.clone().into(),
357                        vec![fix],
358                        description.into(),
359                        source.map(ToOwned::to_owned),
360                    )],
361                    new_point,
362                )
363            } else {
364                let new_indent = SegmentBuilder::whitespace(tables.next_id(), desired_indent);
365
366                if before.is_none() && after.is_none() {
367                    unimplemented!(
368                        "Not set up to handle empty points in this scenario without provided \
369                         before/after anchor: {:?}",
370                        self.segments
371                    );
372                } else if let Some(before) = before {
373                    let fix = LintFix::create_before(
374                        before.clone(),
375                        vec![new_newline.clone(), new_indent.clone()],
376                    );
377
378                    (
379                        vec![LintResult::new(
380                            before.clone().into(),
381                            vec![fix],
382                            Some(format!(
383                                "Expected line break and {} before {:?}",
384                                indent_description(desired_indent),
385                                before.raw()
386                            )),
387                            source.map(ToOwned::to_owned),
388                        )],
389                        ReflowPoint::new(vec![new_newline, new_indent]),
390                    )
391                } else {
392                    let after = after.unwrap();
393                    let fix = LintFix::create_after(
394                        after.clone(),
395                        vec![new_newline.clone(), new_indent.clone()],
396                        None,
397                    );
398                    let description = format!(
399                        "Expected line break and {} after {:?}.",
400                        indent_description(desired_indent),
401                        after.raw()
402                    );
403
404                    (
405                        vec![LintResult::new(
406                            Some(after),
407                            vec![fix],
408                            Some(description),
409                            source.map(ToOwned::to_owned),
410                        )],
411                        ReflowPoint::new(vec![new_newline, new_indent]),
412                    )
413                }
414            }
415        }
416    }
417
418    #[allow(clippy::too_many_arguments)]
419    pub fn respace_point(
420        &self,
421        tables: &Tables,
422        prev_block: Option<&ReflowBlock>,
423        next_block: Option<&ReflowBlock>,
424        root_segment: &ErasedSegment,
425        lint_results: Vec<LintResult>,
426        strip_newlines: bool,
427        anchor_on: &'static str,
428    ) -> (Vec<LintResult>, ReflowPoint) {
429        let mut existing_results = lint_results;
430        let (pre_constraint, post_constraint, strip_newlines) =
431            determine_constraints(prev_block, next_block, strip_newlines);
432
433        // The buffer is used to create the new reflow point to return
434        let (mut segment_buffer, mut last_whitespace, mut new_results) =
435            process_spacing(&self.segments, strip_newlines);
436
437        if let Some((_, whitespace)) = next_block
438            .zip(last_whitespace.clone())
439            .filter(|(next_block, _)| next_block.class_types().contains(SyntaxKind::EndOfFile))
440        {
441            new_results.push(LintResult::new(
442                None,
443                vec![LintFix::delete(whitespace.clone())],
444                Some("Unnecessary trailing whitespace at end of file.".into()),
445                None,
446            ));
447
448            let pos = segment_buffer
449                .iter()
450                .position(|it| it == &whitespace)
451                .unwrap();
452            segment_buffer.remove(pos);
453
454            last_whitespace = None;
455        }
456
457        if segment_buffer
458            .iter()
459            .any(|seg| seg.is_type(SyntaxKind::Newline))
460            && !strip_newlines
461            || (next_block.is_some()
462                && next_block
463                    .unwrap()
464                    .class_types()
465                    .contains(SyntaxKind::EndOfFile))
466        {
467            if let Some(last_whitespace) = last_whitespace {
468                let ws_idx = self
469                    .segments
470                    .iter()
471                    .position(|it| it == &last_whitespace)
472                    .unwrap();
473                if ws_idx > 0 {
474                    let segments_slice = &self.segments[..ws_idx];
475
476                    let prev_seg = segments_slice
477                        .iter()
478                        .rev()
479                        .find(|seg| {
480                            !matches!(seg.get_type(), SyntaxKind::Indent | SyntaxKind::Implicit)
481                        })
482                        .unwrap();
483
484                    if prev_seg.is_type(SyntaxKind::Newline)
485                        && prev_seg.get_end_loc() < last_whitespace.get_start_loc()
486                    {
487                        segment_buffer.remove(ws_idx);
488
489                        let temp_idx = last_whitespace
490                            .get_position_marker()
491                            .unwrap()
492                            .templated_slice
493                            .start;
494
495                        if let Some((index, _)) =
496                            existing_results.iter().enumerate().find(|(_, res)| {
497                                res.anchor
498                                    .as_ref()
499                                    .and_then(|a| a.get_position_marker())
500                                    .is_some_and(|pm| pm.templated_slice.end == temp_idx)
501                            })
502                        {
503                            let mut res = existing_results.remove(index);
504
505                            res.fixes.push(LintFix::delete(last_whitespace));
506                            let new_result = LintResult::new(res.anchor, res.fixes, None, None);
507                            new_results.push(new_result);
508                        } else {
509                            panic!("Could not find removal result.");
510                        }
511                    }
512                }
513            }
514
515            existing_results.extend(new_results);
516            return (existing_results, ReflowPoint::new(segment_buffer));
517        }
518
519        // Do we at least have _some_ whitespace?
520        let segment_buffer = if let Some(last_whitespace) = last_whitespace {
521            // We do - is it the right size?
522            let (segment_buffer, results) = handle_respace_inline_with_space(
523                tables,
524                pre_constraint,
525                post_constraint,
526                prev_block,
527                next_block,
528                root_segment,
529                segment_buffer,
530                last_whitespace,
531            );
532
533            new_results.extend(results);
534            segment_buffer
535        } else {
536            // No. Should we insert some?
537            // NOTE: This method operates on the existing fix buffer.
538            let (segment_buffer, results, _edited) = handle_respace_inline_without_space(
539                tables,
540                pre_constraint,
541                post_constraint,
542                prev_block,
543                next_block,
544                segment_buffer,
545                chain(existing_results, new_results).collect_vec(),
546                anchor_on,
547            );
548
549            existing_results = Vec::new();
550            new_results = results;
551
552            segment_buffer
553        };
554
555        existing_results.extend(new_results);
556        (existing_results, ReflowPoint::new(segment_buffer))
557    }
558
559    pub fn segments(&self) -> &[ErasedSegment] {
560        &self.segments
561    }
562
563    pub fn indent_impulse(&self) -> &IndentStats {
564        self.stats
565            .get_or_init(|| Self::generate_indent_stats(self.segments()))
566    }
567}
568
569fn indent_description(indent: &str) -> String {
570    match indent {
571        "" => "no indent".to_string(),
572        _ if indent.contains(' ') && indent.contains('\t') => "mixed indent".to_string(),
573        _ if indent.starts_with(' ') => {
574            assert!(indent.chars().all(|c| c == ' '));
575            format!("indent of {} spaces", indent.len())
576        }
577        _ if indent.starts_with('\t') => {
578            assert!(indent.chars().all(|c| c == '\t'));
579            format!("indent of {} tabs", indent.len())
580        }
581        _ => panic!("Invalid indent construction: {indent:?}"),
582    }
583}
584
585#[derive(Debug, Clone, Default, PartialEq)]
586pub struct IndentStats {
587    pub impulse: isize,
588    pub trough: isize,
589    pub implicit_indents: Rc<[isize]>,
590}
591
592impl IndentStats {
593    pub fn from_combination(first: Option<IndentStats>, second: &IndentStats) -> Self {
594        match first {
595            Some(first_stats) => IndentStats {
596                impulse: first_stats.impulse + second.impulse,
597                trough: std::cmp::min(first_stats.trough, first_stats.impulse + second.trough),
598                implicit_indents: second.implicit_indents.clone(),
599            },
600            None => second.clone(),
601        }
602    }
603}
604
605#[derive(Debug, PartialEq)]
606pub struct ReflowBlockData {
607    segment: ErasedSegment,
608    spacing_before: Spacing,
609    spacing_after: Spacing,
610    line_position: Option<Vec<LinePosition>>,
611    depth_info: DepthInfo,
612    stack_spacing_configs: IntMap<u64, Spacing>,
613    line_position_configs: IntMap<u64, &'static str>,
614}
615
616#[derive(Debug, PartialEq, Clone)]
617pub struct ReflowBlock {
618    value: Rc<ReflowBlockData>,
619}
620
621impl Deref for ReflowBlock {
622    type Target = ReflowBlockData;
623
624    fn deref(&self) -> &Self::Target {
625        self.value.as_ref()
626    }
627}
628
629impl ReflowBlock {
630    pub fn segment(&self) -> &ErasedSegment {
631        &self.segment
632    }
633
634    pub fn spacing_before(&self) -> Spacing {
635        self.spacing_before
636    }
637
638    pub fn spacing_after(&self) -> Spacing {
639        self.spacing_after
640    }
641
642    pub fn line_position(&self) -> Option<&[LinePosition]> {
643        self.line_position.as_deref()
644    }
645
646    pub fn depth_info(&self) -> &DepthInfo {
647        &self.depth_info
648    }
649
650    pub fn class_types(&self) -> &SyntaxSet {
651        self.segment.class_types()
652    }
653
654    pub fn stack_spacing_configs(&self) -> &IntMap<u64, Spacing> {
655        &self.stack_spacing_configs
656    }
657
658    pub fn line_position_configs(&self) -> &IntMap<u64, &'static str> {
659        &self.line_position_configs
660    }
661}
662
663impl ReflowBlock {
664    pub fn from_config(
665        segment: ErasedSegment,
666        config: &ReflowConfig,
667        depth_info: DepthInfo,
668    ) -> Self {
669        let block_config = config.get_block_config(segment.class_types(), Some(&depth_info));
670
671        let mut stack_spacing_configs = IntMap::default();
672        let mut line_position_configs = IntMap::default();
673
674        for (hash, class_types) in zip(&depth_info.stack_hashes, &depth_info.stack_class_types) {
675            let cfg = config.get_block_config(class_types, None);
676
677            if let Some(spacing_within) = cfg.spacing_within {
678                stack_spacing_configs.insert(*hash, spacing_within);
679            }
680
681            if let Some(line_position) = cfg.line_position {
682                line_position_configs.insert(*hash, line_position);
683            }
684        }
685
686        let line_position = block_config.line_position.map(|line_position| {
687            line_position
688                .split(':')
689                .map(|it| it.parse().unwrap())
690                .collect()
691        });
692
693        Self {
694            value: Rc::new(ReflowBlockData {
695                segment,
696                spacing_before: block_config.spacing_before,
697                spacing_after: block_config.spacing_after,
698                line_position,
699                depth_info,
700                stack_spacing_configs,
701                line_position_configs,
702            }),
703        }
704    }
705}
706
707impl From<ReflowBlock> for ReflowElement {
708    fn from(value: ReflowBlock) -> Self {
709        Self::Block(value)
710    }
711}
712
713impl From<ReflowPoint> for ReflowElement {
714    fn from(value: ReflowPoint) -> Self {
715        Self::Point(value)
716    }
717}
718
719#[derive(Debug, Clone, PartialEq)]
720pub enum ReflowElement {
721    Block(ReflowBlock),
722    Point(ReflowPoint),
723}
724
725impl ReflowElement {
726    pub fn raw(&self) -> String {
727        self.segments().iter().map(|it| it.raw()).join("")
728    }
729
730    pub fn segments(&self) -> &[ErasedSegment] {
731        match self {
732            ReflowElement::Block(block) => std::slice::from_ref(&block.segment),
733            ReflowElement::Point(point) => &point.segments,
734        }
735    }
736
737    pub fn class_types(&self) -> &SyntaxSet {
738        match self {
739            ReflowElement::Block(reflow_block) => reflow_block.class_types(),
740            ReflowElement::Point(reflow_point) => reflow_point.class_types(),
741        }
742    }
743
744    pub fn num_newlines(&self) -> usize {
745        self.segments()
746            .iter()
747            .map(|seg| {
748                let newline_in_class = seg.class_types().contains(SyntaxKind::Newline) as usize;
749
750                let consumed_whitespace = get_consumed_whitespace(seg.into()).unwrap_or_default();
751                newline_in_class + consumed_whitespace.matches('\n').count()
752            })
753            .sum()
754    }
755
756    pub fn as_point(&self) -> Option<&ReflowPoint> {
757        if let Self::Point(v) = self {
758            Some(v)
759        } else {
760            None
761        }
762    }
763
764    pub fn as_block(&self) -> Option<&ReflowBlock> {
765        if let Self::Block(v) = self {
766            Some(v)
767        } else {
768            None
769        }
770    }
771}
772
773impl PartialEq<ReflowBlock> for ReflowElement {
774    fn eq(&self, other: &ReflowBlock) -> bool {
775        match self {
776            ReflowElement::Block(this) => this == other,
777            ReflowElement::Point(_) => false,
778        }
779    }
780}
781
782pub type ReflowSequenceType = Vec<ReflowElement>;