sqruff_lib/utils/reflow/
respace.rs

1use itertools::{Itertools, enumerate};
2use rustc_hash::FxHashMap;
3use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
4use sqruff_lib_core::edit_type::EditType;
5use sqruff_lib_core::lint_fix::LintFix;
6use sqruff_lib_core::parser::markers::PositionMarker;
7use sqruff_lib_core::parser::segments::{ErasedSegment, SegmentBuilder, Tables};
8
9use super::elements::ReflowBlock;
10use crate::core::rules::LintResult;
11use crate::utils::reflow::config::Spacing;
12use crate::utils::reflow::helpers::pretty_segment_name;
13
14fn unpack_constraint(constraint: Spacing, strip_newlines: bool) -> (Spacing, bool) {
15    match constraint {
16        Spacing::TouchInline => (Spacing::Touch, true),
17        Spacing::SingleInline => (Spacing::Single, true),
18        _ => (constraint, strip_newlines),
19    }
20}
21
22pub fn determine_constraints(
23    prev_block: Option<&ReflowBlock>,
24    next_block: Option<&ReflowBlock>,
25    strip_newlines: bool,
26) -> (Spacing, Spacing, bool) {
27    // Start with the defaults
28    let (mut pre_constraint, strip_newlines) = unpack_constraint(
29        if let Some(prev_block) = prev_block {
30            prev_block.spacing_after()
31        } else {
32            Spacing::Single
33        },
34        strip_newlines,
35    );
36
37    let (mut post_constraint, mut strip_newlines) = unpack_constraint(
38        if let Some(next_block) = next_block {
39            next_block.spacing_before()
40        } else {
41            Spacing::Single
42        },
43        strip_newlines,
44    );
45
46    let mut within_spacing = None;
47    let mut idx = None;
48
49    if let Some((prev_block, next_block)) = prev_block.zip(next_block) {
50        let common = prev_block.depth_info().common_with(next_block.depth_info());
51        let last_common = common.last().unwrap();
52        idx = prev_block
53            .depth_info()
54            .stack_hashes
55            .iter()
56            .position(|p| p == last_common)
57            .unwrap()
58            .into();
59
60        let within_constraint = prev_block.stack_spacing_configs().get(last_common);
61        if let Some(within_constraint) = within_constraint {
62            let (within_spacing_inner, strip_newlines_inner) =
63                unpack_constraint(*within_constraint, strip_newlines);
64
65            within_spacing = Some(within_spacing_inner);
66            strip_newlines = strip_newlines_inner;
67        }
68    }
69
70    match within_spacing {
71        Some(Spacing::Touch) => {
72            if pre_constraint != Spacing::Any {
73                pre_constraint = Spacing::Touch;
74            }
75            if post_constraint != Spacing::Any {
76                post_constraint = Spacing::Touch;
77            }
78        }
79        Some(Spacing::Any) => {
80            pre_constraint = Spacing::Any;
81            post_constraint = Spacing::Any;
82        }
83        Some(Spacing::Single) => {}
84        Some(spacing) => {
85            panic!(
86                "Unexpected within constraint: {:?} for {:?}",
87                spacing,
88                prev_block.unwrap().depth_info().stack_class_types[idx.unwrap()]
89            );
90        }
91        _ => {}
92    }
93
94    (pre_constraint, post_constraint, strip_newlines)
95}
96
97pub fn process_spacing(
98    segment_buffer: &[ErasedSegment],
99    strip_newlines: bool,
100) -> (Vec<ErasedSegment>, Option<ErasedSegment>, Vec<LintResult>) {
101    let mut removal_buffer = Vec::new();
102    let mut result_buffer = Vec::new();
103    let mut last_whitespace = Vec::new();
104
105    // Loop through the existing segments looking for spacing.
106    for seg in segment_buffer {
107        // If it's whitespace, store it.
108        if seg.is_type(SyntaxKind::Whitespace) {
109            last_whitespace.push(seg.clone());
110        }
111        // If it's a newline, react accordingly.
112        // NOTE: This should only trigger on literal newlines.
113        else if matches!(seg.get_type(), SyntaxKind::Newline | SyntaxKind::EndOfFile) {
114            if seg
115                .get_position_marker()
116                .is_some_and(|pos_marker| !pos_marker.is_literal())
117            {
118                last_whitespace.clear();
119                continue;
120            }
121
122            if strip_newlines && seg.is_type(SyntaxKind::Newline) {
123                removal_buffer.push(seg.clone());
124                result_buffer.push(LintResult::new(
125                    seg.clone().into(),
126                    vec![LintFix::delete(seg.clone())],
127                    Some("Unexpected line break.".into()),
128                    None,
129                ));
130                continue;
131            }
132
133            if !last_whitespace.is_empty() {
134                for ws in last_whitespace.drain(..) {
135                    removal_buffer.push(ws.clone());
136                    result_buffer.push(LintResult::new(
137                        ws.clone().into(),
138                        vec![LintFix::delete(ws)],
139                        Some("Unnecessary trailing whitespace.".into()),
140                        None,
141                    ))
142                }
143            }
144        }
145    }
146
147    if last_whitespace.len() >= 2 {
148        let seg = segment_buffer.last().unwrap();
149
150        for ws in last_whitespace.iter().skip(1).cloned() {
151            removal_buffer.push(ws.clone());
152            result_buffer.push(LintResult::new(
153                seg.clone().into(),
154                vec![LintFix::delete(ws)],
155                "Unnecessary trailing whitespace.".to_owned().into(),
156                None,
157            ));
158        }
159    }
160
161    // Turn the removal buffer updated segment buffer, last whitespace and
162    // associated fixes.
163
164    let filtered_segment_buffer = segment_buffer
165        .iter()
166        .filter(|s| !removal_buffer.contains(s))
167        .cloned()
168        .collect_vec();
169
170    (
171        filtered_segment_buffer,
172        last_whitespace.first().cloned(),
173        result_buffer,
174    )
175}
176
177fn determine_aligned_inline_spacing(
178    root_segment: &ErasedSegment,
179    whitespace_seg: &ErasedSegment,
180    next_seg: &ErasedSegment,
181    mut next_pos: PositionMarker,
182    segment_type: SyntaxKind,
183    align_within: Option<SyntaxKind>,
184    align_scope: Option<SyntaxKind>,
185) -> String {
186    // Find the level of segment that we're aligning.
187    let mut parent_segment = None;
188
189    // Edge case: if next_seg has no position, we should use the position
190    // of the whitespace for searching.
191    if let Some(align_within) = align_within {
192        for ps in root_segment
193            .path_to(if next_seg.get_position_marker().is_some() {
194                next_seg
195            } else {
196                whitespace_seg
197            })
198            .iter()
199            .rev()
200        {
201            if ps.segment.is_type(align_within) {
202                parent_segment = Some(ps.segment.clone());
203            }
204            if let Some(align_scope) = align_scope
205                && ps.segment.is_type(align_scope)
206            {
207                break;
208            }
209        }
210    }
211
212    if parent_segment.is_none() {
213        return " ".to_string();
214    }
215
216    let parent_segment = parent_segment.unwrap();
217
218    // We've got a parent. Find some siblings.
219    let mut siblings = Vec::new();
220    for sibling in parent_segment.recursive_crawl(
221        &SyntaxSet::single(segment_type),
222        true,
223        &SyntaxSet::EMPTY,
224        true,
225    ) {
226        // Purge any siblings with a boundary between them
227        if align_scope.is_none()
228            || !parent_segment
229                .path_to(&sibling)
230                .iter()
231                .any(|ps| ps.segment.is_type(align_scope.unwrap()))
232        {
233            siblings.push(sibling);
234        }
235    }
236
237    // If the segment we're aligning, has position. Use that position.
238    // If it doesn't, then use the provided one. We can't do sibling analysis
239    // without it.
240    if let Some(pos_marker) = next_seg.get_position_marker() {
241        next_pos = pos_marker.clone();
242    }
243
244    // Purge any siblings which are either self, or on the same line but after it.
245    let mut earliest_siblings: FxHashMap<usize, usize> = FxHashMap::default();
246    siblings.retain(|sibling| {
247        let pos_marker = sibling.get_position_marker().unwrap();
248        let best_seen = earliest_siblings.get(&pos_marker.working_line_no).copied();
249        if let Some(best_seen) = best_seen
250            && pos_marker.working_line_pos > best_seen
251        {
252            return false;
253        }
254        earliest_siblings.insert(pos_marker.working_line_no, pos_marker.working_line_pos);
255
256        if pos_marker.working_line_no == next_pos.working_line_no
257            && pos_marker.working_line_pos != next_pos.working_line_pos
258        {
259            return false;
260        }
261        true
262    });
263
264    // If there's only one sibling, we have nothing to compare to. Default to a
265    // single space.
266    if siblings.len() <= 1 {
267        return " ".to_string();
268    }
269
270    let mut last_code: Option<ErasedSegment> = None;
271    let mut max_desired_line_pos = 0;
272
273    for seg in parent_segment.get_raw_segments() {
274        for sibling in &siblings {
275            if let (Some(seg_pos), Some(sibling_pos)) =
276                (&seg.get_position_marker(), &sibling.get_position_marker())
277                && seg_pos.working_loc() == sibling_pos.working_loc()
278                && let Some(last_code) = &last_code
279            {
280                let loc = last_code
281                    .get_position_marker()
282                    .unwrap()
283                    .working_loc_after(last_code.raw());
284
285                if loc.1 > max_desired_line_pos {
286                    max_desired_line_pos = loc.1;
287                }
288            }
289        }
290
291        if seg.is_code() {
292            last_code = Some(seg.clone());
293        }
294    }
295
296    " ".repeat(
297        1 + max_desired_line_pos
298            - whitespace_seg
299                .get_position_marker()
300                .as_ref()
301                .unwrap()
302                .working_line_pos,
303    )
304}
305
306#[allow(clippy::too_many_arguments)]
307pub fn handle_respace_inline_with_space(
308    tables: &Tables,
309    pre_constraint: Spacing,
310    post_constraint: Spacing,
311    prev_block: Option<&ReflowBlock>,
312    next_block: Option<&ReflowBlock>,
313    root_segment: &ErasedSegment,
314    mut segment_buffer: Vec<ErasedSegment>,
315    last_whitespace: ErasedSegment,
316) -> (Vec<ErasedSegment>, Vec<LintResult>) {
317    // Get some indices so that we can reference around them
318    let ws_idx = segment_buffer
319        .iter()
320        .position(|it| it == &last_whitespace)
321        .unwrap();
322
323    if pre_constraint == Spacing::Any || post_constraint == Spacing::Any {
324        return (segment_buffer, vec![]);
325    }
326
327    if [pre_constraint, post_constraint].contains(&Spacing::Touch) {
328        segment_buffer.remove(ws_idx);
329
330        let description = if let Some(next_block) = next_block {
331            format!(
332                "Unexpected whitespace before {}.",
333                pretty_segment_name(next_block.segment())
334            )
335        } else {
336            "Unexpected whitespace".to_string()
337        };
338
339        let lint_result = LintResult::new(
340            last_whitespace.clone().into(),
341            vec![LintFix::delete(last_whitespace)],
342            Some(description),
343            None,
344        );
345
346        // Return the segment buffer and the lint result
347        return (segment_buffer, vec![lint_result]);
348    }
349
350    // Handle left alignment & singles
351    if (matches!(post_constraint, Spacing::Align { .. }) && next_block.is_some())
352        || pre_constraint == Spacing::Single && post_constraint == Spacing::Single
353    {
354        let (desc, desired_space) = match (post_constraint, next_block) {
355            (
356                Spacing::Align {
357                    seg_type,
358                    within,
359                    scope,
360                },
361                Some(next_block),
362            ) => {
363                let next_pos = if let Some(pos_marker) = next_block.segment().get_position_marker()
364                {
365                    Some(pos_marker.clone())
366                } else if let Some(pos_marker) = last_whitespace.get_position_marker() {
367                    Some(pos_marker.end_point_marker())
368                } else if let Some(prev_block) = prev_block {
369                    prev_block
370                        .segment()
371                        .get_position_marker()
372                        .map(|pos_marker| pos_marker.end_point_marker())
373                } else {
374                    None
375                };
376
377                if let Some(next_pos) = next_pos {
378                    let desired_space = determine_aligned_inline_spacing(
379                        root_segment,
380                        &last_whitespace,
381                        next_block.segment(),
382                        next_pos,
383                        seg_type,
384                        within,
385                        scope,
386                    );
387                    ("Item misaligned".to_string(), desired_space)
388                } else {
389                    ("Item misaligned".to_string(), " ".to_string())
390                }
391            }
392            _ => {
393                let desc = if let Some(next_block) = next_block {
394                    format!(
395                        "Expected only single space before {:?}. Found {:?}.",
396                        next_block.segment().raw(),
397                        last_whitespace.raw()
398                    )
399                } else {
400                    format!(
401                        "Expected only single space. Found {:?}.",
402                        last_whitespace.raw()
403                    )
404                };
405                let desired_space = " ".to_string();
406                (desc, desired_space)
407            }
408        };
409
410        let mut new_results = Vec::new();
411        if last_whitespace.raw().as_str() != desired_space {
412            let new_seg = last_whitespace.edit(tables.next_id(), desired_space.into(), None);
413
414            new_results.push(LintResult::new(
415                last_whitespace.clone().into(),
416                vec![LintFix::replace(
417                    last_whitespace,
418                    vec![new_seg.clone()],
419                    None,
420                )],
421                Some(desc),
422                None,
423            ));
424            segment_buffer[ws_idx] = new_seg;
425        }
426
427        return (segment_buffer, new_results);
428    }
429
430    unimplemented!("Unexpected Constraints: {pre_constraint:?}, {post_constraint:?}");
431}
432
433#[allow(clippy::too_many_arguments)]
434pub fn handle_respace_inline_without_space(
435    tables: &Tables,
436    pre_constraint: Spacing,
437    post_constraint: Spacing,
438    prev_block: Option<&ReflowBlock>,
439    next_block: Option<&ReflowBlock>,
440    mut segment_buffer: Vec<ErasedSegment>,
441    mut existing_results: Vec<LintResult>,
442    anchor_on: &str,
443) -> (Vec<ErasedSegment>, Vec<LintResult>, bool) {
444    let constraints = [Spacing::Touch, Spacing::Any];
445
446    if constraints.contains(&pre_constraint) || constraints.contains(&post_constraint) {
447        return (segment_buffer, existing_results, false);
448    }
449
450    let added_whitespace = SegmentBuilder::whitespace(tables.next_id(), " ");
451
452    // Add it to the buffer first (the easy bit). The hard bit is to then determine
453    // how to generate the appropriate LintFix objects.
454    segment_buffer.push(added_whitespace.clone());
455
456    // So special handling here. If segments either side already exist then we don't
457    // care which we anchor on but if one is already an insertion (as shown by a
458    // lack) of pos_marker, then we should piggyback on that pre-existing fix.
459    let mut existing_fix = None;
460    let mut insertion = None;
461
462    if let Some(block) = prev_block {
463        if block.segment().get_position_marker().is_none() {
464            existing_fix = Some("after");
465            insertion = Some(block.segment().clone());
466        }
467    } else if let Some(block) = next_block
468        && block.segment().get_position_marker().is_none()
469    {
470        existing_fix = Some("before");
471        insertion = Some(block.segment().clone());
472    }
473
474    if let Some(existing_fix) = existing_fix {
475        let mut res_found = None;
476        let mut fix_found = None;
477
478        'outer: for (result_idx, res) in enumerate(&existing_results) {
479            for (fix_idx, fix) in enumerate(&res.fixes) {
480                if fix
481                    .edit
482                    .iter()
483                    .any(|e| e.id() == insertion.as_ref().unwrap().id())
484                {
485                    res_found = Some(result_idx);
486                    fix_found = Some(fix_idx);
487                    break 'outer;
488                }
489            }
490        }
491
492        let res = res_found.unwrap();
493        let fix = fix_found.unwrap();
494
495        let fix = &mut existing_results[res].fixes[fix];
496
497        if existing_fix == "before" {
498            unimplemented!()
499        } else if existing_fix == "after" {
500            fix.edit.push(added_whitespace);
501        }
502
503        return (segment_buffer, existing_results, true);
504    }
505
506    let desc = if let Some((prev_block, next_block)) = prev_block.zip(next_block) {
507        format!(
508            "Expected single whitespace between {:?} and {:?}.",
509            prev_block.segment().raw(),
510            next_block.segment().raw()
511        )
512    } else {
513        "Expected single whitespace.".to_owned()
514    };
515
516    let new_result = if let Some(prev_block) = prev_block
517        && anchor_on != "after"
518    {
519        let anchor = if let Some(block) = next_block {
520            // If next_block is Some, get the first segment
521            block.segment().clone()
522        } else {
523            prev_block.segment().clone()
524        };
525
526        LintResult::new(
527            anchor.into(),
528            vec![LintFix {
529                edit_type: EditType::CreateAfter,
530                anchor: prev_block.segment().clone(),
531                edit: vec![added_whitespace],
532                source: vec![],
533            }],
534            desc.into(),
535            None,
536        )
537    } else if let Some(next_block) = next_block {
538        LintResult::new(
539            next_block.segment().clone().into(),
540            vec![LintFix::create_before(
541                next_block.segment().clone(),
542                vec![SegmentBuilder::whitespace(tables.next_id(), " ")],
543            )],
544            Some(desc),
545            None,
546        )
547    } else {
548        unimplemented!("Not set up to handle a missing _after_ and _before_.")
549    };
550
551    existing_results.push(new_result);
552    (segment_buffer, existing_results, false)
553}
554
555#[cfg(test)]
556mod tests {
557    use itertools::Itertools;
558    use pretty_assertions::assert_eq;
559    use smol_str::ToSmolStr;
560    use sqruff_lib::core::test_functions::parse_ansi_string;
561    use sqruff_lib_core::edit_type::EditType;
562    use sqruff_lib_core::helpers::enter_panic;
563
564    use crate::utils::reflow::helpers::fixes_from_results;
565    use crate::utils::reflow::respace::Tables;
566    use crate::utils::reflow::sequence::{Filter, ReflowSequence};
567
568    #[test]
569    fn test_reflow_sequence_respace() {
570        let cases = [
571            // Basic cases
572            ("select 1+2", (false, Filter::All), "select 1 + 2"),
573            (
574                "select    1   +   2    ",
575                (false, Filter::All),
576                "select 1 + 2",
577            ),
578            // Check newline handling
579            (
580                "select\n    1   +   2",
581                (false, Filter::All),
582                "select\n    1 + 2",
583            ),
584            ("select\n    1   +   2", (true, Filter::All), "select 1 + 2"),
585            // Check filtering
586            (
587                "select  \n  1   +   2 \n ",
588                (false, Filter::All),
589                "select\n  1 + 2\n",
590            ),
591            (
592                "select  \n  1   +   2 \n ",
593                (false, Filter::Inline),
594                "select  \n  1 + 2 \n ",
595            ),
596            (
597                "select  \n  1   +   2 \n ",
598                (false, Filter::Newline),
599                "select\n  1   +   2\n",
600            ),
601        ];
602
603        let tables = Tables::default();
604        for (raw_sql_in, (strip_newlines, filter), raw_sql_out) in cases {
605            let root = parse_ansi_string(raw_sql_in);
606            let config = <_>::default();
607            let seq = ReflowSequence::from_root(root, &config);
608
609            let new_seq = seq.respace(&tables, strip_newlines, filter);
610            assert_eq!(new_seq.raw(), raw_sql_out);
611        }
612    }
613
614    #[test]
615    fn test_reflow_point_respace_point() {
616        let cases = [
617            // Basic cases
618            (
619                "select    1",
620                1,
621                false,
622                " ",
623                vec![(EditType::Replace, "    ".into())],
624            ),
625            (
626                "select 1+2",
627                3,
628                false,
629                " ",
630                vec![(EditType::CreateAfter, "1".into())],
631            ),
632            ("select (1+2)", 3, false, "", vec![]),
633            (
634                "select (  1+2)",
635                3,
636                false,
637                "",
638                vec![(EditType::Delete, "  ".into())],
639            ),
640            // Newline handling
641            ("select\n1", 1, false, "\n", vec![]),
642            ("select\n  1", 1, false, "\n  ", vec![]),
643            (
644                "select  \n  1",
645                1,
646                false,
647                "\n  ",
648                vec![(EditType::Delete, "  ".into())],
649            ),
650            (
651                "select  \n 1",
652                1,
653                true,
654                " ",
655                vec![
656                    (EditType::Delete, "\n".into()),
657                    (EditType::Delete, " ".into()),
658                    (EditType::Replace, "  ".into()),
659                ],
660            ),
661            (
662                "select ( \n  1)",
663                3,
664                true,
665                "",
666                vec![
667                    (EditType::Delete, "\n".into()),
668                    (EditType::Delete, "  ".into()),
669                    (EditType::Delete, " ".into()),
670                ],
671            ),
672        ];
673
674        let tables = Tables::default();
675        for (raw_sql_in, point_idx, strip_newlines, raw_point_sql_out, fixes_out) in cases {
676            let _panic = enter_panic(format!("{raw_sql_in:?}"));
677
678            let root = parse_ansi_string(raw_sql_in);
679            let config = <_>::default();
680            let seq = ReflowSequence::from_root(root.clone(), &config);
681            let pnt = seq.elements()[point_idx].as_point().unwrap();
682
683            let (results, new_pnt) = pnt.respace_point(
684                &tables,
685                seq.elements()[point_idx - 1].as_block(),
686                seq.elements()[point_idx + 1].as_block(),
687                &root,
688                Vec::new(),
689                strip_newlines,
690                "before",
691            );
692
693            assert_eq!(new_pnt.raw(), raw_point_sql_out);
694
695            let fixes = fixes_from_results(results.into_iter())
696                .map(|fix| (fix.edit_type, fix.anchor.raw().to_smolstr()))
697                .collect_vec();
698
699            assert_eq!(fixes, fixes_out);
700        }
701    }
702}