use std::cmp::PartialEq;
use std::str::FromStr;
use sqruff_lib_core::dialects::syntax::SyntaxKind;
use sqruff_lib_core::helpers::capitalize;
use sqruff_lib_core::lint_fix::LintFix;
use sqruff_lib_core::parser::segments::base::{ErasedSegment, Tables};
use strum_macros::{AsRefStr, EnumString};
use super::elements::{ReflowElement, ReflowSequenceType};
use crate::core::rules::base::LintResult;
use crate::utils::reflow::depth_map::StackPositionType;
use crate::utils::reflow::elements::ReflowPoint;
use crate::utils::reflow::helpers::{deduce_line_indent, fixes_from_results};
#[derive(Debug)]
pub struct RebreakSpan {
pub(crate) target: ErasedSegment,
pub(crate) start_idx: usize,
pub(crate) end_idx: usize,
pub(crate) line_position: LinePosition,
pub(crate) strict: bool,
}
#[derive(Debug)]
pub struct RebreakIndices {
_dir: i32,
adj_pt_idx: isize,
newline_pt_idx: isize,
pre_code_pt_idx: isize,
}
impl RebreakIndices {
fn from_elements(elements: &ReflowSequenceType, start_idx: usize, dir: i32) -> Option<Self> {
assert!(dir == 1 || dir == -1);
let limit = if dir == -1 { 0 } else { elements.len() };
let adj_point_idx = start_idx as isize + dir as isize;
if adj_point_idx < 0 || adj_point_idx >= elements.len() as isize {
return None;
}
let mut newline_point_idx = adj_point_idx;
while (dir == 1 && newline_point_idx < limit as isize)
|| (dir == -1 && newline_point_idx >= 0)
{
if elements[newline_point_idx as usize]
.class_types()
.contains(SyntaxKind::Newline)
|| elements[(newline_point_idx + dir as isize) as usize]
.segments()
.iter()
.any(|seg| seg.is_code())
{
break;
}
newline_point_idx += 2 * dir as isize;
}
let mut pre_code_point_idx = newline_point_idx;
while (dir == 1 && pre_code_point_idx < limit as isize)
|| (dir == -1 && pre_code_point_idx >= 0)
{
if elements[(pre_code_point_idx + dir as isize) as usize]
.segments()
.iter()
.any(|seg| seg.is_code())
{
break;
}
pre_code_point_idx += 2 * dir as isize;
}
RebreakIndices {
_dir: dir,
adj_pt_idx: adj_point_idx,
newline_pt_idx: newline_point_idx,
pre_code_pt_idx: pre_code_point_idx,
}
.into()
}
}
#[derive(Debug)]
pub struct RebreakLocation {
target: ErasedSegment,
prev: RebreakIndices,
next: RebreakIndices,
line_position: LinePosition,
strict: bool,
}
#[derive(Debug, PartialEq, Clone, Copy, AsRefStr, EnumString)]
#[strum(serialize_all = "lowercase")]
pub enum LinePosition {
Leading,
Trailing,
Alone,
Strict,
}
impl RebreakLocation {
pub fn from_span(span: RebreakSpan, elements: &ReflowSequenceType) -> Option<Self> {
Self {
target: span.target,
prev: RebreakIndices::from_elements(elements, span.start_idx, -1)?,
next: RebreakIndices::from_elements(elements, span.end_idx, 1)?,
line_position: span.line_position,
strict: span.strict,
}
.into()
}
fn has_inappropriate_newlines(&self, elements: &ReflowSequenceType, strict: bool) -> bool {
let n_prev_newlines = elements[self.prev.newline_pt_idx as usize].num_newlines();
let n_next_newlines = elements[self.next.newline_pt_idx as usize].num_newlines();
let newlines_on_neither_side = n_prev_newlines + n_next_newlines == 0;
let newlines_on_both_sides = n_prev_newlines > 0 && n_next_newlines > 0;
(newlines_on_neither_side && !strict) || newlines_on_both_sides
}
fn pretty_target_name(&self) -> String {
format!("{} {}", self.target.get_type().as_str(), self.target.raw())
}
}
pub fn identify_rebreak_spans(
element_buffer: &ReflowSequenceType,
root_segment: ErasedSegment,
) -> Vec<RebreakSpan> {
let mut spans = Vec::new();
for (idx, elem) in element_buffer
.iter()
.enumerate()
.take(element_buffer.len() - 2)
.skip(2)
{
let ReflowElement::Block(block) = elem else {
continue;
};
if let Some(original_line_position) = block.line_position() {
let line_position = original_line_position.first().unwrap();
spans.push(RebreakSpan {
target: elem.segments().first().cloned().unwrap(),
start_idx: idx,
end_idx: idx,
line_position: *line_position,
strict: original_line_position.last() == Some(&LinePosition::Strict),
});
}
for key in block.line_position_configs().keys() {
let mut final_idx = None;
if block.depth_info().stack_positions[key].idx != 0 {
continue;
}
for end_idx in idx..element_buffer.len() {
let end_elem = &element_buffer[end_idx];
let ReflowElement::Block(end_block) = end_elem else {
continue;
};
if !end_block.depth_info().stack_positions.contains_key(key) {
final_idx = (end_idx - 2).into();
} else if matches!(
end_block.depth_info().stack_positions[key].type_,
Some(StackPositionType::End) | Some(StackPositionType::Solo)
) {
final_idx = end_idx.into();
}
if let Some(final_idx) = final_idx {
let target_depth = block
.depth_info()
.stack_hashes
.iter()
.position(|it| it == key)
.unwrap();
let target = root_segment.path_to(&element_buffer[idx].segments()[0])
[target_depth]
.segment
.clone();
let line_position_configs = block.line_position_configs()[key]
.split(':')
.next()
.unwrap();
let line_position = LinePosition::from_str(line_position_configs).unwrap();
spans.push(RebreakSpan {
target,
start_idx: idx,
end_idx: final_idx,
line_position,
strict: block.line_position_configs()[key].ends_with("strict"),
});
break;
}
}
}
}
spans
}
pub fn rebreak_sequence(
tables: &Tables,
elements: ReflowSequenceType,
root_segment: ErasedSegment,
) -> (ReflowSequenceType, Vec<LintResult>) {
let mut lint_results = Vec::new();
let mut fixes = Vec::new();
let mut elem_buff = elements.clone();
let spans = identify_rebreak_spans(&elem_buff, root_segment.clone());
let mut locations = Vec::new();
for span in spans {
if let Some(loc) = RebreakLocation::from_span(span, &elements) {
locations.push(loc);
}
}
for loc in locations {
if loc.has_inappropriate_newlines(&elements, loc.strict) {
continue;
}
let prev_point = elem_buff[loc.prev.adj_pt_idx as usize]
.as_point()
.unwrap()
.clone();
let next_point = elem_buff[loc.next.adj_pt_idx as usize]
.as_point()
.unwrap()
.clone();
let new_results = if loc.line_position == LinePosition::Leading {
if elem_buff[loc.prev.newline_pt_idx as usize].num_newlines() != 0 {
continue;
}
let pretty_name = loc.pretty_target_name();
let _desc = if loc.strict {
format!(
"{} should always start a new line.",
capitalize(&pretty_name)
)
} else {
format!(
"Found trailing {}. Expected only leading near line breaks.",
pretty_name
)
};
if loc.next.adj_pt_idx == loc.next.pre_code_pt_idx
&& elem_buff[loc.next.newline_pt_idx as usize].num_newlines() == 1
{
let desired_indent = next_point.get_indent().unwrap_or_default();
let (new_results, prev_point) = prev_point.indent_to(
tables,
&desired_indent,
None,
loc.target.clone().into(),
None,
None,
);
let (new_results, next_point) = next_point.respace_point(
tables,
elem_buff[loc.next.adj_pt_idx as usize - 1].as_block(),
elem_buff[loc.next.adj_pt_idx as usize + 1].as_block(),
&root_segment,
new_results,
true,
"before",
);
elem_buff[loc.prev.adj_pt_idx as usize] = prev_point.into();
elem_buff[loc.next.adj_pt_idx as usize] = next_point.into();
new_results
} else {
fixes.push(LintFix::delete(loc.target.clone()));
for seg in elem_buff[loc.prev.adj_pt_idx as usize].segments() {
fixes.push(LintFix::delete(seg.clone()));
}
let (new_results, new_point) = ReflowPoint::new(Vec::new()).respace_point(
tables,
elem_buff[(loc.next.adj_pt_idx - 1) as usize].as_block(),
elem_buff[(loc.next.pre_code_pt_idx + 1) as usize].as_block(),
&root_segment,
Vec::new(),
false,
"after",
);
let mut create_anchor = None;
for i in 0..loc.next.pre_code_pt_idx {
let idx = loc.next.pre_code_pt_idx - i;
if let Some(elem) = elem_buff.get(idx as usize) {
if let Some(segments) = elem.segments().last() {
create_anchor = Some(segments.clone());
break;
}
}
}
if create_anchor.is_none() {
panic!("Could not find anchor for creation.");
}
fixes.push(LintFix::create_after(
create_anchor.unwrap(),
vec![loc.target.clone()],
None,
));
rearrange_and_insert(&mut elem_buff, &loc, new_point);
new_results
}
} else if loc.line_position == LinePosition::Trailing {
if elem_buff[loc.next.newline_pt_idx as usize].num_newlines() != 0 {
continue;
}
let pretty_name = loc.pretty_target_name();
let _desc = if loc.strict {
format!(
"{} should always be at the end of a line.",
capitalize(&pretty_name)
)
} else {
format!(
"Found leading {}. Expected only trailing near line breaks.",
pretty_name
)
};
if loc.prev.adj_pt_idx == loc.prev.pre_code_pt_idx
&& elem_buff[loc.prev.newline_pt_idx as usize].num_newlines() == 1
{
let (new_results, next_point) = next_point.indent_to(
tables,
prev_point.get_indent().as_deref().unwrap_or_default(),
Some(loc.target.clone()),
None,
None,
None,
);
let (new_results, prev_point) = prev_point.respace_point(
tables,
elem_buff[loc.prev.adj_pt_idx as usize - 1].as_block(),
elem_buff[loc.prev.adj_pt_idx as usize + 1].as_block(),
&root_segment,
new_results,
true,
"before",
);
elem_buff[loc.prev.adj_pt_idx as usize] = prev_point.into();
elem_buff[loc.next.adj_pt_idx as usize] = next_point.into();
new_results
} else {
fixes.push(LintFix::delete(loc.target.clone()));
for seg in elem_buff[loc.next.adj_pt_idx as usize].segments() {
fixes.push(LintFix::delete(seg.clone()));
}
let (new_results, new_point) = ReflowPoint::new(Vec::new()).respace_point(
tables,
elem_buff[(loc.prev.pre_code_pt_idx - 1) as usize].as_block(),
elem_buff[(loc.prev.adj_pt_idx + 1) as usize].as_block(),
&root_segment,
Vec::new(),
false,
"before",
);
fixes.push(LintFix::create_before(
elem_buff[loc.prev.pre_code_pt_idx as usize].segments()[0].clone(),
vec![loc.target.clone()],
));
reorder_and_insert(&mut elem_buff, &loc, new_point);
new_results
}
} else if loc.line_position == LinePosition::Alone {
let mut new_results = Vec::new();
if elem_buff[loc.next.newline_pt_idx as usize].num_newlines() == 0 {
let (results, next_point) = next_point.indent_to(
tables,
&deduce_line_indent(
loc.target.get_raw_segments().last().unwrap(),
&root_segment,
),
loc.target.clone().into(),
None,
None,
None,
);
new_results = results;
elem_buff[loc.next.adj_pt_idx as usize] = next_point.into();
}
if elem_buff[loc.prev.adj_pt_idx as usize].num_newlines() == 0 {
let (results, prev_point) = prev_point.indent_to(
tables,
&deduce_line_indent(
loc.target.get_raw_segments().first().unwrap(),
&root_segment,
),
None,
loc.target.clone().into(),
None,
None,
);
new_results = results;
elem_buff[loc.prev.adj_pt_idx as usize] = prev_point.into();
}
new_results
} else {
unimplemented!(
"Unexpected line_position config: {}",
loc.line_position.as_ref()
)
};
let fixes = fixes_from_results(new_results.into_iter())
.chain(std::mem::take(&mut fixes))
.collect();
lint_results.push(LintResult::new(
loc.target.clone().into(),
fixes,
None,
None,
None,
));
}
(elem_buff, lint_results)
}
fn rearrange_and_insert(
elem_buff: &mut Vec<ReflowElement>,
loc: &RebreakLocation,
new_point: ReflowPoint,
) {
let mut new_buff = Vec::with_capacity(elem_buff.len() + 1);
new_buff.extend_from_slice(&elem_buff[..loc.prev.adj_pt_idx as usize]);
new_buff.extend_from_slice(
&elem_buff[loc.next.adj_pt_idx as usize..=loc.next.pre_code_pt_idx as usize],
);
if loc.prev.adj_pt_idx + 1 < loc.next.adj_pt_idx {
new_buff.extend_from_slice(
&elem_buff[loc.prev.adj_pt_idx as usize + 1..loc.next.adj_pt_idx as usize],
);
}
new_buff.push(new_point.into());
if loc.next.pre_code_pt_idx as usize + 1 < elem_buff.len() {
new_buff.extend_from_slice(&elem_buff[loc.next.pre_code_pt_idx as usize + 1..]);
}
*elem_buff = new_buff;
}
fn reorder_and_insert(
elem_buff: &mut Vec<ReflowElement>,
loc: &RebreakLocation,
new_point: ReflowPoint,
) {
let mut new_buff = Vec::with_capacity(elem_buff.len() + 1);
new_buff.extend_from_slice(&elem_buff[..loc.prev.pre_code_pt_idx as usize]);
new_buff.push(new_point.into());
if loc.prev.adj_pt_idx + 1 < loc.next.adj_pt_idx {
new_buff.extend_from_slice(
&elem_buff[loc.prev.adj_pt_idx as usize + 1..loc.next.adj_pt_idx as usize],
);
}
new_buff.extend_from_slice(
&elem_buff[loc.prev.pre_code_pt_idx as usize..=loc.prev.adj_pt_idx as usize],
);
if loc.next.adj_pt_idx as usize + 1 < elem_buff.len() {
new_buff.extend_from_slice(&elem_buff[loc.next.adj_pt_idx as usize + 1..]);
}
*elem_buff = new_buff;
}
#[cfg(test)]
mod tests {
use sqruff_lib::core::test_functions::parse_ansi_string;
use sqruff_lib_core::helpers::enter_panic;
use sqruff_lib_core::parser::segments::base::Tables;
use crate::utils::reflow::sequence::{ReflowSequence, TargetSide};
#[test]
fn test_reflow_sequence_rebreak_root() {
let cases = [
("select 1", "select 1"),
("select 1\n+2", "select 1\n+2"),
("select 1+\n2", "select 1\n+ 2"), ("select\n 1 +\n 2", "select\n 1\n + 2"),
(
"select\n 1 +\n -- comment\n 2",
"select\n 1\n -- comment\n + 2",
),
("select a,b", "select a,b"),
("select a\n,b", "select a,\nb"),
("select\n a\n , b", "select\n a,\n b"),
("select\n a\n , b", "select\n a,\n b"),
("select\n a\n , b", "select\n a,\n b"),
(
"select\n a\n -- comment\n , b",
"select\n a,\n -- comment\n b",
),
];
let tables = Tables::default();
for (raw_sql_in, raw_sql_out) in cases {
let _panic = enter_panic(format!("{raw_sql_in:?}"));
let root = parse_ansi_string(raw_sql_in);
let config = <_>::default();
let seq = ReflowSequence::from_root(root, &config);
let new_seq = seq.rebreak(&tables);
assert_eq!(new_seq.raw(), raw_sql_out);
}
}
#[test]
fn test_reflow_sequence_rebreak_target() {
let cases = [
("select 1+\n(2+3)", 4, "1+\n(", "1\n+ ("),
("select a,\n(b+c)", 4, "a,\n(", "a,\n("),
("select a\n , (b+c)", 6, "a\n , (", "a,\n ("),
("select a,\n(b+c)", 6, ",\n(b", ",\n(b"),
("select a<=b", 4, "a<=", "a<="),
];
let tables = Tables::default();
for (raw_sql_in, target_idx, seq_sql_in, seq_sql_out) in cases {
let root = parse_ansi_string(raw_sql_in);
let target = &root.get_raw_segments()[target_idx];
let config = <_>::default();
let seq = ReflowSequence::from_around_target(target, root, TargetSide::Both, &config);
assert_eq!(seq.raw(), seq_sql_in);
let new_seq = seq.rebreak(&tables);
assert_eq!(new_seq.raw(), seq_sql_out);
}
}
}