Skip to main content

sqruff_lib/utils/reflow/
rebreak.rs

1use std::cmp::PartialEq;
2use std::str::FromStr;
3
4use sqruff_lib_core::dialects::syntax::SyntaxKind;
5use sqruff_lib_core::helpers::capitalize;
6use sqruff_lib_core::lint_fix::LintFix;
7use sqruff_lib_core::parser::segments::{ErasedSegment, Tables};
8use strum_macros::{AsRefStr, EnumString};
9
10use super::elements::{ReflowElement, ReflowSequenceType};
11use crate::core::rules::LintResult;
12use crate::utils::reflow::depth_map::StackPositionType;
13use crate::utils::reflow::elements::ReflowPoint;
14use crate::utils::reflow::helpers::{deduce_line_indent, fixes_from_results, pretty_segment_name};
15
16#[derive(Debug)]
17pub struct RebreakSpan {
18    pub(crate) target: ErasedSegment,
19    pub(crate) start_idx: usize,
20    pub(crate) end_idx: usize,
21    pub(crate) line_position: LinePosition,
22    pub(crate) strict: bool,
23}
24
25#[derive(Debug)]
26pub struct RebreakIndices {
27    _dir: i32,
28    adj_pt_idx: isize,
29    newline_pt_idx: isize,
30    pre_code_pt_idx: isize,
31}
32
33impl RebreakIndices {
34    fn from_elements(elements: &ReflowSequenceType, start_idx: usize, dir: i32) -> Option<Self> {
35        assert!(dir == 1 || dir == -1);
36        let limit = if dir == -1 { 0 } else { elements.len() };
37        let adj_point_idx = start_idx as isize + dir as isize;
38
39        if adj_point_idx < 0 || adj_point_idx >= elements.len() as isize {
40            return None;
41        }
42
43        let mut newline_point_idx = adj_point_idx;
44        while (dir == 1 && newline_point_idx < limit as isize)
45            || (dir == -1 && newline_point_idx >= 0)
46        {
47            // Check bounds for the adjacent element access. When traversing
48            // backward (dir=-1) and reaching index 0, (idx + dir) would be -1
49            // which wraps to usize::MAX causing a panic. Break here as we've
50            // reached the boundary - there's nothing further in this direction.
51            let adjacent_idx = newline_point_idx + dir as isize;
52            if adjacent_idx < 0 || adjacent_idx >= elements.len() as isize {
53                break;
54            }
55            if elements[newline_point_idx as usize]
56                .class_types()
57                .contains(SyntaxKind::Newline)
58                || elements[adjacent_idx as usize]
59                    .segments()
60                    .iter()
61                    .any(|seg| seg.is_code())
62            {
63                break;
64            }
65            newline_point_idx += 2 * dir as isize;
66        }
67
68        // Clamp to the adjacent point if scanning went out of bounds
69        // (no newline or code found in this direction).
70        if newline_point_idx < 0 || newline_point_idx >= elements.len() as isize {
71            newline_point_idx = adj_point_idx;
72        }
73
74        let mut pre_code_point_idx = newline_point_idx;
75        while (dir == 1 && pre_code_point_idx < limit as isize)
76            || (dir == -1 && pre_code_point_idx >= 0)
77        {
78            // Same bounds check as above for the adjacent element access.
79            let adjacent_idx = pre_code_point_idx + dir as isize;
80            if adjacent_idx < 0 || adjacent_idx >= elements.len() as isize {
81                break;
82            }
83            if elements[adjacent_idx as usize]
84                .segments()
85                .iter()
86                .any(|seg| seg.is_code())
87            {
88                break;
89            }
90            pre_code_point_idx += 2 * dir as isize;
91        }
92
93        // Clamp to the newline point if scanning went out of bounds.
94        if pre_code_point_idx < 0 || pre_code_point_idx >= elements.len() as isize {
95            pre_code_point_idx = newline_point_idx;
96        }
97
98        RebreakIndices {
99            _dir: dir,
100            adj_pt_idx: adj_point_idx,
101            newline_pt_idx: newline_point_idx,
102            pre_code_pt_idx: pre_code_point_idx,
103        }
104        .into()
105    }
106}
107
108#[derive(Debug)]
109pub struct RebreakLocation {
110    target: ErasedSegment,
111    prev: RebreakIndices,
112    next: RebreakIndices,
113    line_position: LinePosition,
114    strict: bool,
115}
116
117#[derive(Debug, PartialEq, Eq, Clone, Copy, AsRefStr, EnumString)]
118#[strum(serialize_all = "lowercase")]
119pub enum LinePosition {
120    Leading,
121    Trailing,
122    Alone,
123    Strict,
124}
125
126impl RebreakLocation {
127    /// Expand a span to a location.
128    pub fn from_span(span: RebreakSpan, elements: &ReflowSequenceType) -> Option<Self> {
129        Self {
130            target: span.target,
131            prev: RebreakIndices::from_elements(elements, span.start_idx, -1)?,
132            next: RebreakIndices::from_elements(elements, span.end_idx, 1)?,
133            line_position: span.line_position,
134            strict: span.strict,
135        }
136        .into()
137    }
138
139    fn has_inappropriate_newlines(&self, elements: &ReflowSequenceType, strict: bool) -> bool {
140        let n_prev_newlines = elements[self.prev.newline_pt_idx as usize].num_newlines();
141        let n_next_newlines = elements[self.next.newline_pt_idx as usize].num_newlines();
142
143        let newlines_on_neither_side = n_prev_newlines + n_next_newlines == 0;
144        let newlines_on_both_sides = n_prev_newlines > 0 && n_next_newlines > 0;
145
146        (newlines_on_neither_side && !strict) || newlines_on_both_sides
147    }
148
149    fn pretty_target_name(&self) -> String {
150        pretty_segment_name(&self.target)
151    }
152}
153
154pub fn identify_rebreak_spans(
155    element_buffer: &ReflowSequenceType,
156    root_segment: &ErasedSegment,
157) -> Vec<RebreakSpan> {
158    let mut spans = Vec::new();
159
160    for (idx, elem) in element_buffer
161        .iter()
162        .enumerate()
163        .take(element_buffer.len() - 2)
164        .skip(2)
165    {
166        let ReflowElement::Block(block) = elem else {
167            continue;
168        };
169
170        if let Some(original_line_position) = block.line_position() {
171            let Some(pos_marker) = elem
172                .segments()
173                .first()
174                .and_then(|seg| seg.get_position_marker())
175            else {
176                continue;
177            };
178            if !pos_marker.is_literal() {
179                continue;
180            }
181
182            spans.push(RebreakSpan {
183                target: elem.segments().first().cloned().unwrap(),
184                start_idx: idx,
185                end_idx: idx,
186                line_position: original_line_position.position(),
187                strict: original_line_position.is_strict(),
188            });
189        }
190
191        for key in block.line_position_configs().keys() {
192            let mut final_idx = None;
193            if block.depth_info().stack_positions[key].idx != 0 {
194                continue;
195            }
196
197            for (end_idx, end_elem) in element_buffer.iter().enumerate().skip(idx) {
198                let ReflowElement::Block(end_block) = end_elem else {
199                    continue;
200                };
201
202                if !end_block.depth_info().stack_positions.contains_key(key) {
203                    // Left the scope. The last block inside is two positions back.
204                    if final_idx.is_none() {
205                        final_idx = (end_idx - 2).into();
206                    }
207                    break;
208                } else if matches!(
209                    end_block.depth_info().stack_positions[key].type_,
210                    Some(StackPositionType::End) | Some(StackPositionType::Solo)
211                ) {
212                    // Track the latest End/Solo block but keep scanning,
213                    // because multiple blocks within the last child all
214                    // have End type at the parent level.
215                    final_idx = end_idx.into();
216                }
217            }
218
219            if let Some(final_idx) = final_idx {
220                let target_depth = block
221                    .depth_info()
222                    .stack_hashes
223                    .iter()
224                    .position(|it| it == key)
225                    .unwrap();
226                let target = root_segment.path_to(&element_buffer[idx].segments()[0])[target_depth]
227                    .segment
228                    .clone();
229
230                let line_position_config = block.line_position_configs()[key];
231
232                spans.push(RebreakSpan {
233                    target,
234                    start_idx: idx,
235                    end_idx: final_idx,
236                    line_position: line_position_config.position(),
237                    strict: line_position_config.is_strict(),
238                });
239            }
240        }
241    }
242
243    spans
244}
245
246pub fn identify_keyword_rebreak_spans(element_buffer: &ReflowSequenceType) -> Vec<RebreakSpan> {
247    let mut spans = Vec::new();
248
249    for idx in 2..element_buffer.len().saturating_sub(2) {
250        let ReflowElement::Block(block) = &element_buffer[idx] else {
251            continue;
252        };
253
254        for key in block.keyword_line_position_configs().keys() {
255            let line_position_config = &block.keyword_line_position_configs()[key];
256            if line_position_config.eq_ignore_ascii_case("none") {
257                continue;
258            }
259
260            if block.depth_info().stack_positions[key].idx > 1 {
261                continue;
262            }
263
264            let configured_depth = block
265                .depth_info()
266                .stack_hashes
267                .iter()
268                .position(|it| it == key)
269                .unwrap();
270            if block.depth_info().stack_depth > configured_depth + 1 {
271                continue;
272            }
273
274            if element_buffer[idx].segments().is_empty()
275                || !element_buffer[idx].segments()[0].is_type(SyntaxKind::Keyword)
276            {
277                continue;
278            }
279
280            for end_idx in idx..element_buffer.len() {
281                let end_elem = &element_buffer[end_idx];
282                let final_idx = match end_elem {
283                    ReflowElement::Point(point)
284                        if point.segments().iter().any(|seg| seg.is_indent()) =>
285                    {
286                        Some(end_idx - 1)
287                    }
288                    ReflowElement::Point(_) => continue,
289                    ReflowElement::Block(end_block)
290                        if matches!(
291                            end_block.depth_info().stack_positions[key].type_,
292                            Some(StackPositionType::End) | Some(StackPositionType::Solo)
293                        ) =>
294                    {
295                        Some(end_idx)
296                    }
297                    ReflowElement::Block(_) => continue,
298                };
299
300                if let Some(final_idx) = final_idx {
301                    let parent_exclusion = block
302                        .keyword_line_position_exclusions_configs()
303                        .get(key)
304                        .cloned()
305                        .unwrap_or_default();
306                    if block
307                        .depth_info()
308                        .stack_class_types
309                        .iter()
310                        .any(|class_types| class_types.intersects(&parent_exclusion))
311                    {
312                        break;
313                    }
314
315                    let line_position =
316                        LinePosition::from_str(line_position_config.split(':').next().unwrap())
317                            .unwrap();
318
319                    spans.push(RebreakSpan {
320                        target: element_buffer[idx].segments()[0].clone(),
321                        start_idx: idx,
322                        end_idx: final_idx,
323                        line_position,
324                        strict: line_position_config.ends_with("strict"),
325                    });
326                    break;
327                }
328            }
329        }
330    }
331
332    spans
333}
334
335fn locations_from_spans(
336    spans: Vec<RebreakSpan>,
337    elements: &ReflowSequenceType,
338) -> Vec<RebreakLocation> {
339    spans
340        .into_iter()
341        .filter_map(|span| RebreakLocation::from_span(span, elements))
342        .collect()
343}
344
345pub fn rebreak_sequence(
346    tables: &Tables,
347    elements: ReflowSequenceType,
348    root_segment: &ErasedSegment,
349) -> (ReflowSequenceType, Vec<LintResult>) {
350    let mut lint_results = Vec::new();
351    let mut fixes = Vec::new();
352    let mut elem_buff = elements.clone();
353
354    // Given a sequence we should identify the objects which
355    // make sense to rebreak. That includes any raws with config,
356    // but also and parent segments which have config and we can
357    // find both ends for. Given those spans, we then need to find
358    // the points either side of them and then the blocks either
359    // side to respace them at the same time.
360
361    // 1. First find appropriate spans.
362    let spans = identify_rebreak_spans(&elem_buff, root_segment);
363    let locations = locations_from_spans(spans, &elements);
364
365    // Handle each span:
366    for loc in locations {
367        if loc.has_inappropriate_newlines(&elements, loc.strict) {
368            continue;
369        }
370
371        // if loc.has_templated_newline(elem_buff) {
372        //     continue;
373        // }
374
375        // Points and blocks either side are just offsets from the indices.
376        let prev_point = elem_buff[loc.prev.adj_pt_idx as usize]
377            .as_point()
378            .unwrap()
379            .clone();
380        let next_point = elem_buff[loc.next.adj_pt_idx as usize]
381            .as_point()
382            .unwrap()
383            .clone();
384
385        // So we know we have a preference, is it ok?
386        let new_results = if loc.line_position == LinePosition::Leading {
387            if elem_buff[loc.prev.newline_pt_idx as usize].num_newlines() != 0 {
388                // We're good. It's already leading.
389                continue;
390            }
391
392            // Generate the text for any issues.
393            let pretty_name = loc.pretty_target_name();
394            let _desc = if loc.strict {
395                format!(
396                    "{} should always start a new line.",
397                    capitalize(&pretty_name)
398                )
399            } else {
400                format!("Found trailing {pretty_name}. Expected only leading near line breaks.")
401            };
402
403            if loc.next.adj_pt_idx == loc.next.pre_code_pt_idx
404                && elem_buff[loc.next.newline_pt_idx as usize].num_newlines() == 1
405            {
406                // Simple case. No comments.
407                // Strip newlines from the next point.
408                // Apply the indent to the previous point.
409
410                let desired_indent = next_point.get_indent().unwrap_or_default();
411
412                let (new_results, prev_point) = prev_point.indent_to(
413                    tables,
414                    &desired_indent,
415                    None,
416                    loc.target.clone().into(),
417                    None,
418                    None,
419                );
420
421                let (new_results, next_point) = next_point.respace_point(
422                    tables,
423                    elem_buff[loc.next.adj_pt_idx as usize - 1].as_block(),
424                    elem_buff[loc.next.adj_pt_idx as usize + 1].as_block(),
425                    root_segment,
426                    new_results,
427                    true,
428                    "before",
429                );
430
431                // Update the points in the buffer
432                elem_buff[loc.prev.adj_pt_idx as usize] = prev_point.into();
433                elem_buff[loc.next.adj_pt_idx as usize] = next_point.into();
434
435                new_results
436            } else {
437                fixes.push(LintFix::delete(loc.target.clone()));
438                for seg in elem_buff[loc.prev.adj_pt_idx as usize].segments() {
439                    fixes.push(LintFix::delete(seg.clone()));
440                }
441
442                let (new_results, new_point) = ReflowPoint::new(Vec::new()).respace_point(
443                    tables,
444                    elem_buff[(loc.next.adj_pt_idx - 1) as usize].as_block(),
445                    elem_buff[(loc.next.pre_code_pt_idx + 1) as usize].as_block(),
446                    root_segment,
447                    Vec::new(),
448                    false,
449                    "after",
450                );
451
452                let mut create_anchor = None;
453                for i in 0..loc.next.pre_code_pt_idx {
454                    let idx = loc.next.pre_code_pt_idx - i;
455                    if let Some(elem) = elem_buff.get(idx as usize)
456                        && let Some(segments) = elem.segments().last()
457                    {
458                        create_anchor = Some(segments.clone());
459                        break;
460                    }
461                }
462
463                if create_anchor.is_none() {
464                    panic!("Could not find anchor for creation.");
465                }
466
467                fixes.push(LintFix::create_after(
468                    create_anchor.unwrap(),
469                    vec![loc.target.clone()],
470                    None,
471                ));
472
473                rearrange_and_insert(&mut elem_buff, &loc, new_point);
474
475                new_results
476            }
477        } else if loc.line_position == LinePosition::Trailing {
478            if elem_buff[loc.next.newline_pt_idx as usize].num_newlines() != 0 {
479                continue;
480            }
481
482            let pretty_name = loc.pretty_target_name();
483            let _desc = if loc.strict {
484                format!(
485                    "{} should always be at the end of a line.",
486                    capitalize(&pretty_name)
487                )
488            } else {
489                format!("Found leading {pretty_name}. Expected only trailing near line breaks.")
490            };
491
492            if loc.prev.adj_pt_idx == loc.prev.pre_code_pt_idx
493                && elem_buff[loc.prev.newline_pt_idx as usize].num_newlines() == 1
494            {
495                let (new_results, next_point) = next_point.indent_to(
496                    tables,
497                    prev_point.get_indent().as_deref().unwrap_or_default(),
498                    Some(loc.target.clone()),
499                    None,
500                    None,
501                    None,
502                );
503
504                let (new_results, prev_point) = prev_point.respace_point(
505                    tables,
506                    elem_buff[loc.prev.adj_pt_idx as usize - 1].as_block(),
507                    elem_buff[loc.prev.adj_pt_idx as usize + 1].as_block(),
508                    root_segment,
509                    new_results,
510                    true,
511                    "before",
512                );
513
514                // Update the points in the buffer
515                elem_buff[loc.prev.adj_pt_idx as usize] = prev_point.into();
516                elem_buff[loc.next.adj_pt_idx as usize] = next_point.into();
517
518                new_results
519            } else {
520                fixes.push(LintFix::delete(loc.target.clone()));
521                for seg in elem_buff[loc.next.adj_pt_idx as usize].segments() {
522                    fixes.push(LintFix::delete(seg.clone()));
523                }
524
525                let (new_results, new_point) = ReflowPoint::new(Vec::new()).respace_point(
526                    tables,
527                    elem_buff[(loc.prev.pre_code_pt_idx - 1) as usize].as_block(),
528                    elem_buff[(loc.prev.adj_pt_idx + 1) as usize].as_block(),
529                    root_segment,
530                    Vec::new(),
531                    false,
532                    "before",
533                );
534
535                fixes.push(LintFix::create_before(
536                    elem_buff[loc.prev.pre_code_pt_idx as usize].segments()[0].clone(),
537                    vec![loc.target.clone()],
538                ));
539
540                reorder_and_insert(&mut elem_buff, &loc, new_point);
541
542                new_results
543            }
544        } else if loc.line_position == LinePosition::Alone {
545            let mut new_results = Vec::new();
546
547            let needs_next_newline =
548                elem_buff[loc.next.newline_pt_idx as usize].num_newlines() == 0;
549            let needs_prev_newline = elem_buff[loc.prev.adj_pt_idx as usize].num_newlines() == 0;
550
551            // Don't add a newline before a statement terminator (semicolon).
552            // The terminator should stay on the same line as the preceding
553            // clause content (e.g. `WHERE a = 1;` not `WHERE a = 1\n;`).
554            let next_code_idx = (loc.next.pre_code_pt_idx + 1) as usize;
555            let next_is_statement_terminator = next_code_idx < elem_buff.len()
556                && elem_buff[next_code_idx]
557                    .segments()
558                    .iter()
559                    .any(|seg| seg.get_type() == SyntaxKind::StatementTerminator);
560
561            let skip_next_newline = needs_next_newline && next_is_statement_terminator;
562
563            if (!needs_next_newline || skip_next_newline) && !needs_prev_newline {
564                continue;
565            }
566
567            if needs_next_newline && !skip_next_newline {
568                let (results, next_point) = next_point.indent_to(
569                    tables,
570                    &deduce_line_indent(
571                        loc.target.get_raw_segments().last().unwrap(),
572                        root_segment,
573                    ),
574                    loc.target.clone().into(),
575                    None,
576                    None,
577                    None,
578                );
579
580                new_results = results;
581                elem_buff[loc.next.adj_pt_idx as usize] = next_point.into();
582            }
583
584            if needs_prev_newline {
585                let (results, prev_point) = prev_point.indent_to(
586                    tables,
587                    &deduce_line_indent(
588                        loc.target.get_raw_segments().first().unwrap(),
589                        root_segment,
590                    ),
591                    None,
592                    loc.target.clone().into(),
593                    None,
594                    None,
595                );
596
597                new_results = results;
598                elem_buff[loc.prev.adj_pt_idx as usize] = prev_point.into();
599            }
600
601            new_results
602        } else {
603            unimplemented!(
604                "Unexpected line_position config: {}",
605                loc.line_position.as_ref()
606            )
607        };
608
609        let fixes = fixes_from_results(new_results.into_iter())
610            .chain(std::mem::take(&mut fixes))
611            .collect();
612        lint_results.push(LintResult::new(
613            loc.target.clone().into(),
614            fixes,
615            None,
616            None,
617        ));
618    }
619
620    (elem_buff, lint_results)
621}
622
623pub fn rebreak_keywords_sequence(
624    tables: &Tables,
625    elements: ReflowSequenceType,
626    root_segment: &ErasedSegment,
627) -> (ReflowSequenceType, Vec<LintResult>) {
628    let mut lint_results = Vec::new();
629    let mut fixes = Vec::new();
630    let mut elem_buff = elements.clone();
631
632    let spans = identify_keyword_rebreak_spans(&elem_buff);
633    let locations = locations_from_spans(spans, &elements);
634
635    for loc in locations {
636        if loc.has_inappropriate_newlines(&elem_buff, true) {
637            continue;
638        }
639
640        let prev_point = elem_buff[loc.prev.adj_pt_idx as usize]
641            .as_point()
642            .unwrap()
643            .clone();
644        let next_point = elem_buff[loc.next.adj_pt_idx as usize]
645            .as_point()
646            .unwrap()
647            .clone();
648
649        let (desc, new_results) = if loc.line_position == LinePosition::Leading {
650            if elem_buff[loc.prev.newline_pt_idx as usize].num_newlines() != 0 {
651                continue;
652            }
653
654            let pretty_name = loc.pretty_target_name();
655            let desc = format!("The {pretty_name} should always start a new line.");
656
657            let (new_results, prev_point) = prev_point.indent_to(
658                tables,
659                next_point.get_indent().as_deref().unwrap_or_default(),
660                None,
661                elem_buff[loc.prev.adj_pt_idx as usize + 1]
662                    .segments()
663                    .first()
664                    .cloned(),
665                None,
666                None,
667            );
668            let (new_results, next_point) = next_point.respace_point(
669                tables,
670                elem_buff[loc.next.adj_pt_idx as usize - 1].as_block(),
671                elem_buff[loc.next.adj_pt_idx as usize + 1].as_block(),
672                root_segment,
673                new_results,
674                true,
675                "before",
676            );
677
678            elem_buff[loc.prev.adj_pt_idx as usize] = prev_point.into();
679            elem_buff[loc.next.adj_pt_idx as usize] = next_point.into();
680
681            (desc, new_results)
682        } else if loc.line_position == LinePosition::Trailing {
683            if elem_buff[loc.next.newline_pt_idx as usize].num_newlines() != 0 {
684                continue;
685            }
686
687            let pretty_name = loc.pretty_target_name();
688            let desc = format!("The {pretty_name} should always be at the end of a line.");
689
690            let (new_results, next_point) = next_point.indent_to(
691                tables,
692                prev_point.get_indent().as_deref().unwrap_or_default(),
693                Some(
694                    elem_buff[loc.next.adj_pt_idx as usize - 1]
695                        .segments()
696                        .last()
697                        .cloned()
698                        .unwrap(),
699                ),
700                None,
701                None,
702                None,
703            );
704            let (new_results, prev_point) = prev_point.respace_point(
705                tables,
706                elem_buff[loc.prev.adj_pt_idx as usize - 1].as_block(),
707                elem_buff[loc.prev.adj_pt_idx as usize + 1].as_block(),
708                root_segment,
709                new_results,
710                true,
711                "before",
712            );
713
714            elem_buff[loc.prev.adj_pt_idx as usize] = prev_point.into();
715            elem_buff[loc.next.adj_pt_idx as usize] = next_point.into();
716
717            (desc, new_results)
718        } else if loc.line_position == LinePosition::Alone {
719            let pretty_name = loc.pretty_target_name();
720            let desc =
721                format!("The {pretty_name} should always have a line break both before and after.");
722            let mut new_results = Vec::new();
723
724            if elem_buff[loc.next.newline_pt_idx as usize].num_newlines() == 0 {
725                let (results, next_point) = next_point.indent_to(
726                    tables,
727                    prev_point.get_indent().as_deref().unwrap_or_default(),
728                    Some(
729                        elem_buff[loc.next.adj_pt_idx as usize - 1]
730                            .segments()
731                            .last()
732                            .cloned()
733                            .unwrap(),
734                    ),
735                    None,
736                    None,
737                    None,
738                );
739                new_results = results;
740                elem_buff[loc.next.adj_pt_idx as usize] = next_point.into();
741            }
742
743            if elem_buff[loc.prev.adj_pt_idx as usize].num_newlines() == 0 {
744                let (results, prev_point) = prev_point.indent_to(
745                    tables,
746                    next_point.get_indent().as_deref().unwrap_or_default(),
747                    None,
748                    elem_buff[loc.prev.adj_pt_idx as usize + 1]
749                        .segments()
750                        .first()
751                        .cloned(),
752                    None,
753                    None,
754                );
755                new_results = results;
756                elem_buff[loc.prev.adj_pt_idx as usize] = prev_point.into();
757            }
758
759            (desc, new_results)
760        } else {
761            unimplemented!(
762                "Unexpected line_position config: {}",
763                loc.line_position.as_ref()
764            )
765        };
766
767        let fixes = fixes_from_results(new_results.into_iter())
768            .chain(std::mem::take(&mut fixes))
769            .collect();
770        lint_results.push(LintResult::new(
771            loc.target.clone().into(),
772            fixes,
773            Some(desc),
774            None,
775        ));
776    }
777
778    (elem_buff, lint_results)
779}
780
781fn rearrange_and_insert(
782    elem_buff: &mut Vec<ReflowElement>,
783    loc: &RebreakLocation,
784    new_point: ReflowPoint,
785) {
786    let mut new_buff = Vec::with_capacity(elem_buff.len() + 1);
787
788    // First segment: up to loc.prev.adj_pt_idx (exclusive)
789    new_buff.extend_from_slice(&elem_buff[..loc.prev.adj_pt_idx as usize]);
790
791    // Second segment: loc.next.adj_pt_idx to loc.next.pre_code_pt_idx (inclusive)
792    new_buff.extend_from_slice(
793        &elem_buff[loc.next.adj_pt_idx as usize..=loc.next.pre_code_pt_idx as usize],
794    );
795
796    // Third segment: loc.prev.adj_pt_idx + 1 to loc.next.adj_pt_idx (exclusive, the
797    // target)
798    if loc.prev.adj_pt_idx + 1 < loc.next.adj_pt_idx {
799        new_buff.extend_from_slice(
800            &elem_buff[loc.prev.adj_pt_idx as usize + 1..loc.next.adj_pt_idx as usize],
801        );
802    }
803
804    // Insert new_point here
805    new_buff.push(new_point.into());
806
807    // Last segment: after loc.next.pre_code_pt_idx
808    if loc.next.pre_code_pt_idx as usize + 1 < elem_buff.len() {
809        new_buff.extend_from_slice(&elem_buff[loc.next.pre_code_pt_idx as usize + 1..]);
810    }
811
812    // Replace old buffer with the new one
813    *elem_buff = new_buff;
814}
815
816fn reorder_and_insert(
817    elem_buff: &mut Vec<ReflowElement>,
818    loc: &RebreakLocation,
819    new_point: ReflowPoint,
820) {
821    let mut new_buff = Vec::with_capacity(elem_buff.len() + 1);
822
823    // First segment: up to loc.prev.pre_code_pt_idx (exclusive)
824    new_buff.extend_from_slice(&elem_buff[..loc.prev.pre_code_pt_idx as usize]);
825
826    // Insert new_point here
827    new_buff.push(new_point.into());
828
829    // Second segment: loc.prev.adj_pt_idx + 1 to loc.next.adj_pt_idx (exclusive,
830    // the target)
831    if loc.prev.adj_pt_idx + 1 < loc.next.adj_pt_idx {
832        new_buff.extend_from_slice(
833            &elem_buff[loc.prev.adj_pt_idx as usize + 1..loc.next.adj_pt_idx as usize],
834        );
835    }
836
837    // Third segment: loc.prev.pre_code_pt_idx to loc.prev.adj_pt_idx + 1
838    // (inclusive)
839    new_buff.extend_from_slice(
840        &elem_buff[loc.prev.pre_code_pt_idx as usize..=loc.prev.adj_pt_idx as usize],
841    );
842
843    // Last segment: after loc.next.adj_pt_idx
844    if loc.next.adj_pt_idx as usize + 1 < elem_buff.len() {
845        new_buff.extend_from_slice(&elem_buff[loc.next.adj_pt_idx as usize + 1..]);
846    }
847
848    // Replace old buffer with the new one
849    *elem_buff = new_buff;
850}
851
852#[cfg(test)]
853mod tests {
854    use sqruff_lib::core::test_functions::parse_ansi_string;
855    use sqruff_lib_core::helpers::enter_panic;
856    use sqruff_lib_core::parser::segments::Tables;
857
858    use crate::utils::reflow::sequence::{RebreakType, ReflowSequence, TargetSide};
859
860    #[test]
861    fn test_reflow_sequence_rebreak_root() {
862        let cases = [
863            // Trivial Case
864            ("select 1", "select 1"),
865            // These rely on the default config being for leading operators
866            ("select 1\n+2", "select 1\n+2"),
867            ("select 1+\n2", "select 1\n+ 2"), // NOTE: Implicit respace.
868            ("select\n  1 +\n  2", "select\n  1\n  + 2"),
869            (
870                "select\n  1 +\n  -- comment\n  2",
871                "select\n  1\n  -- comment\n  + 2",
872            ),
873            // These rely on the default config being for trailing commas
874            ("select a,b", "select a,b"),
875            ("select a\n,b", "select a,\nb"),
876            ("select\n  a\n  , b", "select\n  a,\n  b"),
877            ("select\n    a\n    , b", "select\n    a,\n    b"),
878            ("select\n  a\n    , b", "select\n  a,\n    b"),
879            (
880                "select\n  a\n  -- comment\n  , b",
881                "select\n  a,\n  -- comment\n  b",
882            ),
883        ];
884
885        let tables = Tables::default();
886        for (raw_sql_in, raw_sql_out) in cases {
887            let _panic = enter_panic(format!("{raw_sql_in:?}"));
888
889            let root = parse_ansi_string(raw_sql_in);
890            let config = <_>::default();
891            let seq = ReflowSequence::from_root(&root, &config);
892            let new_seq = seq.rebreak(&tables, RebreakType::Lines);
893
894            assert_eq!(new_seq.raw(), raw_sql_out);
895        }
896    }
897
898    #[test]
899    fn test_reflow_sequence_rebreak_target() {
900        let cases = [
901            ("select 1+\n(2+3)", 4, "1+\n(", "1\n+ ("),
902            ("select a,\n(b+c)", 4, "a,\n(", "a,\n("),
903            ("select a\n  , (b+c)", 6, "a\n  , (", "a,\n  ("),
904            // Here we don't have enough context to rebreak it so
905            // it should be left unaltered.
906            ("select a,\n(b+c)", 6, ",\n(b", ",\n(b"),
907            // This intentionally targets an incomplete span.
908            ("select a<=b", 4, "a<=", "a<="),
909        ];
910
911        let tables = Tables::default();
912        for (raw_sql_in, target_idx, seq_sql_in, seq_sql_out) in cases {
913            let root = parse_ansi_string(raw_sql_in);
914            let target = &root.get_raw_segments()[target_idx];
915            let config = <_>::default();
916            let seq = ReflowSequence::from_around_target(target, &root, TargetSide::Both, &config);
917
918            assert_eq!(seq.raw(), seq_sql_in);
919
920            let new_seq = seq.rebreak(&tables, RebreakType::Lines);
921            assert_eq!(new_seq.raw(), seq_sql_out);
922        }
923    }
924}