sqruff_lib/rules/layout/
lt08.rsuse std::iter::repeat;
use ahash::AHashMap;
use itertools::Itertools;
use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
use sqruff_lib_core::edit_type::EditType;
use sqruff_lib_core::helpers::IndexMap;
use sqruff_lib_core::lint_fix::LintFix;
use sqruff_lib_core::parser::segments::base::SegmentBuilder;
use crate::core::config::Value;
use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
use crate::core::rules::context::RuleContext;
use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
#[derive(Debug, Default, Clone)]
pub struct RuleLT08;
impl Rule for RuleLT08 {
fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
Ok(RuleLT08.erased())
}
fn name(&self) -> &'static str {
"layout.cte_newline"
}
fn description(&self) -> &'static str {
"Blank line expected but not found after CTE closing bracket."
}
fn long_description(&self) -> &'static str {
r#"
**Anti-pattern**
There is no blank line after the CTE closing bracket. In queries with many CTEs, this hinders readability.
```sql
WITH plop AS (
SELECT * FROM foo
)
SELECT a FROM plop
```
**Best practice**
Add a blank line.
```sql
WITH plop AS (
SELECT * FROM foo
)
SELECT a FROM plop
```
"#
}
fn groups(&self) -> &'static [RuleGroups] {
&[RuleGroups::All, RuleGroups::Core, RuleGroups::Layout]
}
fn eval(&self, context: RuleContext) -> Vec<LintResult> {
let mut error_buffer = Vec::new();
let global_comma_style = context.config.raw["layout"]["type"]["comma"]["line_position"]
.as_string()
.unwrap();
let expanded_segments = context.segment.iter_segments(
const { &SyntaxSet::new(&[SyntaxKind::CommonTableExpression]) },
false,
);
let bracket_indices = expanded_segments
.iter()
.enumerate()
.filter_map(|(idx, seg)| seg.is_type(SyntaxKind::Bracketed).then_some(idx));
for bracket_idx in bracket_indices {
let forward_slice = &expanded_segments[bracket_idx..];
let mut seg_idx = 1;
let mut line_idx: usize = 0;
let mut comma_seg_idx = 0;
let mut blank_lines = 0;
let mut comma_line_idx = None;
let mut line_blank = false;
let mut line_starts = IndexMap::default();
let mut comment_lines = Vec::new();
while forward_slice[seg_idx].is_type(SyntaxKind::Comma)
|| !forward_slice[seg_idx].is_code()
{
if forward_slice[seg_idx].is_type(SyntaxKind::Newline) {
if line_blank {
blank_lines += 1;
}
line_blank = true;
line_idx += 1;
line_starts.insert(line_idx, seg_idx + 1);
} else if forward_slice[seg_idx].is_type(SyntaxKind::Comment)
|| forward_slice[seg_idx].is_type(SyntaxKind::InlineComment)
|| forward_slice[seg_idx].is_type(SyntaxKind::BlockComment)
{
line_blank = false;
comment_lines.push(line_idx);
} else if forward_slice[seg_idx].is_type(SyntaxKind::Comma) {
comma_line_idx = line_idx.into();
comma_seg_idx = seg_idx;
}
seg_idx += 1;
}
let comma_style = if comma_line_idx.is_none() {
"final"
} else if line_idx == 0 {
"oneline"
} else if let Some(0) = comma_line_idx {
"trailing"
} else if let Some(idx) = comma_line_idx {
if idx == line_idx {
"leading"
} else {
"floating"
}
} else {
"floating"
};
if blank_lines >= 1 {
continue;
}
let mut fix_type = EditType::CreateBefore;
let mut fix_point = None;
let num_newlines = if comma_style == "oneline" {
if global_comma_style == "trailing" {
fix_point = forward_slice[comma_seg_idx + 1].clone().into();
if forward_slice[comma_seg_idx + 1].is_type(SyntaxKind::Whitespace) {
fix_type = EditType::Replace;
}
} else if global_comma_style == "leading" {
fix_point = forward_slice[comma_seg_idx].clone().into();
} else {
unimplemented!("Unexpected global comma style {global_comma_style:?}");
}
2
} else {
if comment_lines.is_empty() || !comment_lines.contains(&(line_idx - 1)) {
if matches!(comma_style, "trailing" | "final" | "floating") {
if forward_slice[seg_idx - 1].is_type(SyntaxKind::Whitespace) {
fix_point = forward_slice[seg_idx - 1].clone().into();
fix_type = EditType::Replace;
} else {
fix_point = forward_slice[seg_idx].clone().into();
}
}
} else if comma_style == "leading" {
fix_point = forward_slice[comma_seg_idx].clone().into();
} else {
let mut offset = 1;
while line_idx
.checked_sub(offset)
.map_or(false, |idx| comment_lines.contains(&idx))
{
offset += 1;
}
let mut effective_line_idx = line_idx - (offset - 1);
if effective_line_idx == 0 {
effective_line_idx = line_idx;
}
let line_start_idx = if effective_line_idx < line_starts.len() {
*line_starts.get(&effective_line_idx).unwrap()
} else {
let (_, line_start) = line_starts.last().unwrap_or((&0, &0));
*line_start
};
fix_point = forward_slice[line_start_idx].clone().into();
}
1
};
let fixes = vec![LintFix {
edit_type: fix_type,
anchor: fix_point.unwrap(),
edit: repeat(SegmentBuilder::newline(context.tables.next_id(), "\n"))
.take(num_newlines)
.collect_vec()
.into(),
source: Vec::new(),
}];
error_buffer.push(LintResult::new(
forward_slice[seg_idx].clone().into(),
fixes,
None,
None,
None,
));
}
error_buffer
}
fn is_fix_compatible(&self) -> bool {
true
}
fn crawl_behaviour(&self) -> Crawler {
SegmentSeekerCrawler::new(const { SyntaxSet::new(&[SyntaxKind::WithCompoundStatement]) })
.into()
}
}