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 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 for seg in segment_buffer {
107 if seg.is_type(SyntaxKind::Whitespace) {
109 last_whitespace.push(seg.clone());
110 }
111 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 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 let mut parent_segment = None;
188
189 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 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 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 let Some(pos_marker) = next_seg.get_position_marker() {
241 next_pos = pos_marker.clone();
242 }
243
244 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 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 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 (segment_buffer, vec![lint_result]);
348 }
349
350 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 segment_buffer.push(added_whitespace.clone());
455
456 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 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 ("select 1+2", (false, Filter::All), "select 1 + 2"),
573 (
574 "select 1 + 2 ",
575 (false, Filter::All),
576 "select 1 + 2",
577 ),
578 (
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 (
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 (
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 ("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}