use std::cell::OnceCell;
use std::iter::zip;
use std::ops::Deref;
use std::rc::Rc;
use itertools::{chain, Itertools};
use nohash_hasher::IntMap;
use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
use sqruff_lib_core::lint_fix::LintFix;
use sqruff_lib_core::parser::segments::base::{ErasedSegment, SegmentBuilder, Tables};
use super::config::{ReflowConfig, Spacing};
use super::depth_map::DepthInfo;
use super::respace::determine_constraints;
use crate::core::rules::base::LintResult;
use crate::utils::reflow::rebreak::LinePosition;
use crate::utils::reflow::respace::{
handle_respace_inline_with_space, handle_respace_inline_without_space, process_spacing,
};
fn get_consumed_whitespace(segment: Option<&ErasedSegment>) -> Option<String> {
let segment = segment?;
if segment.is_type(SyntaxKind::Placeholder) {
None
} else {
None
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ReflowPointData {
segments: Vec<ErasedSegment>,
stats: IndentStats,
class_types: OnceCell<SyntaxSet>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ReflowPoint {
value: Rc<ReflowPointData>,
}
impl Deref for ReflowPoint {
type Target = ReflowPointData;
fn deref(&self) -> &Self::Target {
self.value.as_ref()
}
}
impl ReflowPoint {
pub fn new(segments: Vec<ErasedSegment>) -> Self {
let stats = Self::generate_indent_stats(&segments);
Self {
value: Rc::new(ReflowPointData {
segments,
stats,
class_types: OnceCell::new(),
}),
}
}
pub fn raw(&self) -> String {
self.segments.iter().map(|it| it.raw()).join("")
}
pub fn class_types(&self) -> &SyntaxSet {
self.class_types.get_or_init(|| {
self.segments
.iter()
.flat_map(|it| it.class_types())
.collect()
})
}
fn generate_indent_stats(segments: &[ErasedSegment]) -> IndentStats {
let mut trough = 0;
let mut running_sum = 0;
let mut implicit_indents = Vec::new();
for seg in segments {
if seg.is_indent() {
running_sum += seg.indent_val() as isize;
if seg.is_type(SyntaxKind::Implicit) {
implicit_indents.push(running_sum);
}
}
if running_sum < trough {
trough = running_sum
}
}
IndentStats {
impulse: running_sum,
trough,
implicit_indents,
}
}
pub fn get_indent_segment(&self) -> Option<ErasedSegment> {
let mut indent = None;
for seg in self.segments.iter().rev() {
if seg
.get_position_marker()
.filter(|pos_marker| !pos_marker.is_literal())
.is_some()
{
continue;
} else if seg.is_type(SyntaxKind::Newline) {
return indent;
} else if seg.is_whitespace() {
indent = Some(seg.clone());
} else if get_consumed_whitespace(Some(seg))
.unwrap_or_default()
.contains('\n')
{
return Some(seg.clone());
}
}
indent
}
pub(crate) fn num_newlines(&self) -> usize {
self.segments
.iter()
.map(|seg| {
let newline_in_class = seg.class_types().contains(SyntaxKind::Newline) as usize;
let consumed_whitespace = get_consumed_whitespace(seg.into()).unwrap_or_default();
newline_in_class + consumed_whitespace.matches('\n').count()
})
.sum()
}
pub fn get_indent(&self) -> Option<String> {
if self.num_newlines() == 0 {
return None;
}
let seg = self.get_indent_segment();
let consumed_whitespace = get_consumed_whitespace(seg.as_ref());
if let Some(consumed_whitespace) = consumed_whitespace {
return consumed_whitespace
.split('\n')
.last()
.unwrap()
.to_owned()
.into();
}
if let Some(seg) = seg {
Some(seg.raw().to_string())
} else {
String::new().into()
}
}
pub fn indent_to(
&self,
tables: &Tables,
desired_indent: &str,
after: Option<ErasedSegment>,
before: Option<ErasedSegment>,
description: Option<&str>,
source: Option<&str>,
) -> (Vec<LintResult>, ReflowPoint) {
assert!(
!desired_indent.contains('\n'),
"Newline found in desired indent."
);
let indent_seg = self.get_indent_segment();
if indent_seg
.as_ref()
.filter(|indent_seg| indent_seg.is_type(SyntaxKind::Placeholder))
.is_some()
{
unimplemented!()
} else if self.num_newlines() != 0 {
if let Some(indent_seg) = indent_seg {
if indent_seg.raw() == desired_indent {
unimplemented!()
} else if desired_indent.is_empty() {
let idx = self
.segments
.iter()
.position(|seg| seg == &indent_seg)
.unwrap();
return (
vec![LintResult::new(
indent_seg.clone().into(),
vec![LintFix::delete(indent_seg.clone())],
None,
Some(
description
.map(ToOwned::to_owned)
.unwrap_or_else(|| "Line should not be indented.".to_owned())
.to_string(),
),
source.map(|s| s.to_string()),
)],
ReflowPoint::new(
self.segments[..idx]
.iter()
.chain(self.segments[idx + 1..].iter())
.cloned()
.collect(),
),
);
};
let new_indent =
indent_seg.edit(tables.next_id(), desired_indent.to_owned().into(), None);
let idx = self
.segments
.iter()
.position(|it| it == &indent_seg)
.unwrap();
let description = format!("Expected {}.", indent_description(desired_indent));
let lint_result = LintResult::new(
indent_seg.clone().into(),
vec![LintFix::replace(indent_seg, vec![new_indent.clone()], None)],
None,
description.into(),
None,
);
let mut new_segments = Vec::new();
new_segments.extend_from_slice(&self.segments[..idx]);
new_segments.push(new_indent);
new_segments.extend_from_slice(&self.segments[idx + 1..]);
let new_reflow_point = ReflowPoint::new(new_segments);
(vec![lint_result], new_reflow_point)
} else {
if desired_indent.is_empty() {
return (Vec::new(), self.clone());
}
let new_indent = SegmentBuilder::whitespace(tables.next_id(), desired_indent);
let (last_newline_idx, last_newline) = self
.segments
.iter()
.enumerate()
.rev()
.find(|(_, it)| {
it.is_type(SyntaxKind::Newline)
&& it.get_position_marker().unwrap().is_literal()
})
.unwrap();
let mut new_segments = self.segments[..=last_newline_idx].to_vec();
new_segments.push(new_indent.clone());
new_segments.extend_from_slice(&self.segments[last_newline_idx + 1..]);
return (
vec![LintResult::new(
if let Some(before) = before {
before.into()
} else {
unimplemented!()
},
vec![LintFix::replace(
last_newline.clone(),
vec![last_newline.clone(), new_indent],
None,
)],
None,
format!("Expected {}", indent_description(desired_indent)).into(),
None,
)],
ReflowPoint::new(new_segments),
);
}
} else {
let new_newline = SegmentBuilder::newline(tables.next_id(), "\n");
let ws_seg = self
.segments
.iter()
.find(|seg| seg.is_type(SyntaxKind::Whitespace));
if let Some(ws_seg) = ws_seg {
let new_segs = if desired_indent.is_empty() {
vec![new_newline]
} else {
vec![
new_newline,
ws_seg.edit(tables.next_id(), desired_indent.to_owned().into(), None),
]
};
let idx = self.segments.iter().position(|it| ws_seg == it).unwrap();
let description = if let Some(before_seg) = before {
format!(
"Expected line break and {} before {:?}.",
indent_description(desired_indent),
before_seg.raw()
)
} else if let Some(after_seg) = after {
format!(
"Expected line break and {} after {:?}.",
indent_description(desired_indent),
after_seg.raw()
)
} else {
format!(
"Expected line break and {}.",
indent_description(desired_indent)
)
};
let fix = LintFix::replace(ws_seg.clone(), new_segs.clone(), None);
let new_point = ReflowPoint::new({
let mut new_segments = Vec::new();
if idx > 0 {
new_segments.extend_from_slice(&self.segments[..idx]);
}
new_segments.extend(new_segs);
if idx < self.segments.len() {
new_segments.extend_from_slice(&self.segments[idx + 1..]);
}
new_segments
});
return (
vec![LintResult::new(
ws_seg.clone().into(),
vec![fix],
None,
description.into(),
source.map(ToOwned::to_owned),
)],
new_point,
);
} else {
let new_indent = SegmentBuilder::whitespace(tables.next_id(), desired_indent);
if before.is_none() && after.is_none() {
unimplemented!(
"Not set up to handle empty points in this scenario without provided \
before/after anchor: {:?}",
self.segments
);
} else if let Some(before) = before {
let fix = LintFix::create_before(
before.clone(),
vec![new_newline.clone(), new_indent.clone()],
);
return (
vec![LintResult::new(
before.clone().into(),
vec![fix],
None,
Some(format!(
"Expected line break and {} before {:?}",
indent_description(desired_indent),
before.raw()
)),
source.map(ToOwned::to_owned),
)],
ReflowPoint::new(vec![new_newline, new_indent]),
);
} else {
let after = after.unwrap();
let fix = LintFix::create_after(
after.clone(),
vec![new_newline.clone(), new_indent.clone()],
None,
);
let description = format!(
"Expected line break and {} after {:?}.",
indent_description(desired_indent),
after.raw()
);
return (
vec![LintResult::new(
Some(after),
vec![fix],
None,
Some(description),
source.map(ToOwned::to_owned),
)],
ReflowPoint::new(vec![new_newline, new_indent]),
);
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn respace_point(
&self,
tables: &Tables,
prev_block: Option<&ReflowBlock>,
next_block: Option<&ReflowBlock>,
root_segment: &ErasedSegment,
lint_results: Vec<LintResult>,
strip_newlines: bool,
anchor_on: &'static str,
) -> (Vec<LintResult>, ReflowPoint) {
let mut existing_results = lint_results;
let (pre_constraint, post_constraint, strip_newlines) =
determine_constraints(prev_block, next_block, strip_newlines);
let (mut segment_buffer, mut last_whitespace, mut new_results) =
process_spacing(&self.segments, strip_newlines);
if let Some((_, whitespace)) = next_block
.zip(last_whitespace.clone())
.filter(|(next_block, _)| next_block.class_types().contains(SyntaxKind::EndOfFile))
{
new_results.push(LintResult::new(
None,
vec![LintFix::delete(whitespace.clone())],
None,
Some("Unnecessary trailing whitespace at end of file.".into()),
None,
));
let pos = segment_buffer
.iter()
.position(|it| it == &whitespace)
.unwrap();
segment_buffer.remove(pos);
last_whitespace = None;
}
if segment_buffer
.iter()
.any(|seg| seg.is_type(SyntaxKind::Newline))
&& !strip_newlines
|| (next_block.is_some()
&& next_block
.unwrap()
.class_types()
.contains(SyntaxKind::EndOfFile))
{
if let Some(last_whitespace) = last_whitespace {
let ws_idx = self
.segments
.iter()
.position(|it| it == &last_whitespace)
.unwrap();
if ws_idx > 0 {
let segments_slice = &self.segments[..ws_idx];
let prev_seg = segments_slice
.iter()
.rev()
.find(|seg| {
!matches!(seg.get_type(), SyntaxKind::Indent | SyntaxKind::Implicit)
})
.unwrap();
if prev_seg.is_type(SyntaxKind::Newline)
&& prev_seg.get_end_loc() < last_whitespace.get_start_loc()
{
segment_buffer.remove(ws_idx);
let temp_idx = last_whitespace
.get_position_marker()
.unwrap()
.templated_slice
.start;
if let Some((index, _)) =
existing_results.iter().enumerate().find(|(_, res)| {
res.anchor
.as_ref()
.and_then(|a| a.get_position_marker())
.map_or(false, |pm| pm.templated_slice.end == temp_idx)
})
{
let mut res = existing_results.remove(index);
res.fixes.push(LintFix::delete(last_whitespace));
let new_result =
LintResult::new(res.anchor, res.fixes, None, None, None);
new_results.push(new_result);
} else {
panic!("Could not find removal result.");
}
}
}
}
existing_results.extend(new_results);
return (existing_results, ReflowPoint::new(segment_buffer));
}
let segment_buffer = if let Some(last_whitespace) = last_whitespace {
let (segment_buffer, results) = handle_respace_inline_with_space(
tables,
pre_constraint,
post_constraint,
prev_block,
next_block,
root_segment,
segment_buffer,
last_whitespace,
);
new_results.extend(results);
segment_buffer
} else {
let (segment_buffer, results, _edited) = handle_respace_inline_without_space(
tables,
pre_constraint,
post_constraint,
prev_block,
next_block,
segment_buffer,
chain(existing_results, new_results).collect_vec(),
anchor_on,
);
existing_results = Vec::new();
new_results = results;
segment_buffer
};
existing_results.extend(new_results);
(existing_results, ReflowPoint::new(segment_buffer))
}
pub fn segments(&self) -> &[ErasedSegment] {
&self.segments
}
pub fn indent_impulse(&self) -> &IndentStats {
&self.stats
}
}
fn indent_description(indent: &str) -> String {
match indent {
"" => "no indent".to_string(),
_ if indent.contains(' ') && indent.contains('\t') => "mixed indent".to_string(),
_ if indent.starts_with(' ') => {
assert!(indent.chars().all(|c| c == ' '));
format!("indent of {} spaces", indent.len())
}
_ if indent.starts_with('\t') => {
assert!(indent.chars().all(|c| c == '\t'));
format!("indent of {} tabs", indent.len())
}
_ => panic!("Invalid indent construction: {:?}", indent),
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct IndentStats {
pub impulse: isize,
pub trough: isize,
pub implicit_indents: Vec<isize>,
}
impl IndentStats {
pub fn from_combination(first: Option<IndentStats>, second: &IndentStats) -> Self {
match first {
Some(first_stats) => IndentStats {
impulse: first_stats.impulse + second.impulse,
trough: std::cmp::min(first_stats.trough, first_stats.impulse + second.trough),
implicit_indents: second.implicit_indents.clone(),
},
None => second.clone(),
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct ReflowBlockData {
segment: ErasedSegment,
spacing_before: Spacing,
spacing_after: Spacing,
line_position: Option<Vec<LinePosition>>,
depth_info: DepthInfo,
stack_spacing_configs: IntMap<u64, Spacing>,
line_position_configs: IntMap<u64, &'static str>,
}
#[derive(Debug, PartialEq, Clone)]
pub struct ReflowBlock {
value: Rc<ReflowBlockData>,
}
impl Deref for ReflowBlock {
type Target = ReflowBlockData;
fn deref(&self) -> &Self::Target {
self.value.as_ref()
}
}
impl ReflowBlock {
pub fn segment(&self) -> &ErasedSegment {
&self.segment
}
pub fn spacing_before(&self) -> Spacing {
self.spacing_before
}
pub fn spacing_after(&self) -> Spacing {
self.spacing_after
}
pub fn line_position(&self) -> Option<&[LinePosition]> {
self.line_position.as_deref()
}
pub fn depth_info(&self) -> &DepthInfo {
&self.depth_info
}
pub fn class_types(&self) -> &SyntaxSet {
self.segment.class_types()
}
pub fn stack_spacing_configs(&self) -> &IntMap<u64, Spacing> {
&self.stack_spacing_configs
}
pub fn line_position_configs(&self) -> &IntMap<u64, &'static str> {
&self.line_position_configs
}
}
impl ReflowBlock {
pub fn from_config(
segment: ErasedSegment,
config: &ReflowConfig,
depth_info: DepthInfo,
) -> Self {
let block_config = config.get_block_config(segment.class_types(), Some(&depth_info));
let mut stack_spacing_configs = IntMap::default();
let mut line_position_configs = IntMap::default();
for (hash, class_types) in zip(&depth_info.stack_hashes, &depth_info.stack_class_types) {
let cfg = config.get_block_config(class_types, None);
if let Some(spacing_within) = cfg.spacing_within {
stack_spacing_configs.insert(*hash, spacing_within);
}
if let Some(line_position) = cfg.line_position {
line_position_configs.insert(*hash, line_position);
}
}
let line_position = block_config.line_position.map(|line_position| {
line_position
.split(':')
.map(|it| it.parse().unwrap())
.collect()
});
Self {
value: Rc::new(ReflowBlockData {
segment,
spacing_before: block_config.spacing_before,
spacing_after: block_config.spacing_after,
line_position,
depth_info,
stack_spacing_configs,
line_position_configs,
}),
}
}
}
impl From<ReflowBlock> for ReflowElement {
fn from(value: ReflowBlock) -> Self {
Self::Block(value)
}
}
impl From<ReflowPoint> for ReflowElement {
fn from(value: ReflowPoint) -> Self {
Self::Point(value)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ReflowElement {
Block(ReflowBlock),
Point(ReflowPoint),
}
impl ReflowElement {
pub fn raw(&self) -> String {
self.segments().iter().map(|it| it.raw()).join("")
}
pub fn segments(&self) -> &[ErasedSegment] {
match self {
ReflowElement::Block(block) => std::slice::from_ref(&block.segment),
ReflowElement::Point(point) => &point.segments,
}
}
pub fn class_types(&self) -> &SyntaxSet {
match self {
ReflowElement::Block(reflow_block) => reflow_block.class_types(),
ReflowElement::Point(reflow_point) => reflow_point.class_types(),
}
}
pub fn num_newlines(&self) -> usize {
self.segments()
.iter()
.map(|seg| {
let newline_in_class = seg.class_types().contains(SyntaxKind::Newline) as usize;
let consumed_whitespace = get_consumed_whitespace(seg.into()).unwrap_or_default();
newline_in_class + consumed_whitespace.matches('\n').count()
})
.sum()
}
pub fn as_point(&self) -> Option<&ReflowPoint> {
if let Self::Point(v) = self {
Some(v)
} else {
None
}
}
pub fn as_block(&self) -> Option<&ReflowBlock> {
if let Self::Block(v) = self {
Some(v)
} else {
None
}
}
}
impl PartialEq<ReflowBlock> for ReflowElement {
fn eq(&self, other: &ReflowBlock) -> bool {
match self {
ReflowElement::Block(this) => this == other,
ReflowElement::Point(_) => false,
}
}
}
pub type ReflowSequenceType = Vec<ReflowElement>;