use ahash::{AHashMap, AHashSet};
use itertools::Itertools;
use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
use sqruff_lib_core::edit_type::EditType;
use sqruff_lib_core::lint_fix::LintFix;
use sqruff_lib_core::parser::segments::base::{ErasedSegment, SegmentBuilder, Tables};
use sqruff_lib_core::utils::functional::segments::Segments;
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, RootOnlyCrawler};
#[derive(Default, Clone, Debug)]
pub struct RuleCV06 {
multiline_newline: bool,
require_final_semicolon: bool,
}
impl Rule for RuleCV06 {
fn load_from_config(&self, config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
let multiline_newline = config["multiline_newline"].as_bool().unwrap();
let require_final_semicolon = config["require_final_semicolon"].as_bool().unwrap();
Ok(Self {
multiline_newline,
require_final_semicolon,
}
.erased())
}
fn name(&self) -> &'static str {
"convention.terminator"
}
fn description(&self) -> &'static str {
"Statements must end with a semi-colon."
}
fn long_description(&self) -> &'static str {
r"
**Anti-pattern**
A statement is not immediately terminated with a semi-colon. The `•` represents space.
```sql
SELECT
a
FROM foo
;
SELECT
b
FROM bar••;
```
**Best practice**
Immediately terminate the statement with a semi-colon.
```sql
SELECT
a
FROM foo;
```"
}
fn groups(&self) -> &'static [RuleGroups] {
&[RuleGroups::All, RuleGroups::Convention]
}
fn eval(&self, context: RuleContext) -> Vec<LintResult> {
debug_assert!(context.segment.is_type(SyntaxKind::File));
let mut results = vec![];
for (idx, segment) in context.segment.segments().iter().enumerate() {
let mut res = None;
if segment.is_type(SyntaxKind::StatementTerminator) {
res =
self.handle_semicolon(context.tables, segment.clone(), context.segment.clone());
} else if self.require_final_semicolon && idx == context.segment.segments().len() - 1 {
res = self.ensure_final_semicolon(context.tables, context.segment.clone());
}
if let Some(res) = res {
results.push(res);
}
}
results
}
fn is_fix_compatible(&self) -> bool {
true
}
fn crawl_behaviour(&self) -> Crawler {
RootOnlyCrawler.into()
}
}
impl RuleCV06 {
fn handle_trailing_inline_comments(
parent_segment: ErasedSegment,
anchor_segment: ErasedSegment,
) -> ErasedSegment {
for comment_segment in parent_segment
.recursive_crawl(
const {
&SyntaxSet::new(&[
SyntaxKind::Comment,
SyntaxKind::InlineComment,
SyntaxKind::BlockComment,
])
},
true,
&SyntaxSet::EMPTY,
false,
)
.iter()
{
assert!(comment_segment.get_position_marker().is_some());
assert!(anchor_segment.get_position_marker().is_some());
if comment_segment
.get_position_marker()
.unwrap()
.working_line_no
== anchor_segment
.get_position_marker()
.unwrap()
.working_line_no
&& !comment_segment.is_type(SyntaxKind::BlockComment)
{
return comment_segment.clone();
}
}
anchor_segment
}
fn is_one_line_statement(parent_segment: ErasedSegment, segment: ErasedSegment) -> bool {
let statement_segment = parent_segment
.path_to(&segment)
.iter()
.filter(|&it| it.segment.is_type(SyntaxKind::Statement))
.map(|it| it.segment.clone())
.next();
match statement_segment {
None => false,
Some(statement_segment) => statement_segment
.recursive_crawl(
const { &SyntaxSet::new(&[SyntaxKind::Newline]) },
true,
&SyntaxSet::EMPTY,
true,
)
.is_empty(),
}
}
fn handle_semicolon(
&self,
tables: &Tables,
target_segment: ErasedSegment,
parent_segment: ErasedSegment,
) -> Option<LintResult> {
let info = Self::get_segment_move_context(target_segment.clone(), parent_segment.clone());
let semicolon_newline = if !info.is_one_line {
self.multiline_newline
} else {
false
};
if !semicolon_newline {
self.handle_semicolon_same_line(tables, target_segment, parent_segment, info)
} else {
self.handle_semicolon_newline(tables, target_segment, parent_segment, info)
}
}
fn handle_semicolon_same_line(
&self,
tables: &Tables,
target_segment: ErasedSegment,
parent_segment: ErasedSegment,
info: SegmentMoveContext,
) -> Option<LintResult> {
if info.before_segment.is_empty() {
return None;
}
let fixes = self.create_semicolon_and_delete_whitespace(
target_segment,
parent_segment,
info.anchor_segment.clone(),
info.whitespace_deletions,
vec![
SegmentBuilder::token(tables.next_id(), ";", SyntaxKind::StatementTerminator)
.finish(),
],
);
Some(LintResult::new(
Some(info.anchor_segment),
fixes,
None,
None,
None,
))
}
fn handle_preceding_inline_comments(
before_segment: Segments,
anchor_segment: ErasedSegment,
) -> (Segments, ErasedSegment) {
let same_line_comment = before_segment.iter().find(|s| {
s.is_comment()
&& !s.is_type(SyntaxKind::BlockComment)
&& s.get_position_marker().is_some()
&& s.get_position_marker().unwrap().working_loc().0
== anchor_segment
.get_raw_segments()
.last()
.unwrap()
.get_position_marker()
.unwrap()
.working_loc()
.0
});
if let Some(same_line_comment) = same_line_comment {
let anchor_segment = same_line_comment.clone();
let before_segment = before_segment
.iter()
.take_while(|s| *s != same_line_comment)
.cloned()
.collect();
let before_segment = Segments::from_vec(before_segment, None);
(before_segment, anchor_segment)
} else {
(before_segment, anchor_segment)
}
}
fn handle_semicolon_newline(
&self,
tables: &Tables,
target_segment: ErasedSegment,
parent_segment: ErasedSegment,
info: SegmentMoveContext,
) -> Option<LintResult> {
let (before_segment, anchor_segment) = Self::handle_preceding_inline_comments(
info.before_segment.clone(),
info.anchor_segment.clone(),
);
if before_segment.len() == 1
&& before_segment.all(Some(|segment: &ErasedSegment| {
segment.is_type(SyntaxKind::Newline)
}))
{
return None;
}
let anchor_segment =
Self::handle_trailing_inline_comments(parent_segment.clone(), anchor_segment.clone());
let fixes = if anchor_segment == target_segment {
vec![LintFix::replace(
anchor_segment.clone(),
vec![
SegmentBuilder::whitespace(tables.next_id(), "\n"),
SegmentBuilder::token(tables.next_id(), ";", SyntaxKind::StatementTerminator)
.finish(),
],
None,
)]
} else {
self.create_semicolon_and_delete_whitespace(
target_segment,
parent_segment,
anchor_segment.clone(),
info.whitespace_deletions.clone(),
vec![
SegmentBuilder::newline(tables.next_id(), "\n"),
SegmentBuilder::token(tables.next_id(), ";", SyntaxKind::StatementTerminator)
.finish(),
],
)
};
Some(LintResult::new(
Some(anchor_segment),
fixes,
None,
None,
None,
))
}
fn create_semicolon_and_delete_whitespace(
&self,
target_segment: ErasedSegment,
parent_segment: ErasedSegment,
anchor_segment: ErasedSegment,
mut whitespace_deletions: Segments,
create_segments: Vec<ErasedSegment>,
) -> Vec<LintFix> {
let anchor_segment = choose_anchor_segment(
&parent_segment,
EditType::CreateAfter,
&anchor_segment,
true,
);
let mut lintfix_fn: fn(
ErasedSegment,
Vec<ErasedSegment>,
Option<Vec<ErasedSegment>>,
) -> LintFix = LintFix::create_after;
if AHashSet::from_iter(whitespace_deletions.base.clone()).contains(&anchor_segment) {
lintfix_fn = LintFix::replace;
whitespace_deletions = whitespace_deletions.select(
Some(|it: &ErasedSegment| it.id() != anchor_segment.id()),
None,
None,
None,
);
}
let mut fixes = vec![
lintfix_fn(anchor_segment, create_segments, None),
LintFix::delete(target_segment),
];
fixes.extend(whitespace_deletions.into_iter().map(LintFix::delete));
fixes
}
fn ensure_final_semicolon(
&self,
tables: &Tables,
parent_segment: ErasedSegment,
) -> Option<LintResult> {
let mut anchor_segment = parent_segment.segments().last().cloned();
let trigger_segment = parent_segment.segments().last().cloned();
let mut semi_colon_exist_flag = false;
let mut is_one_line = false;
let mut before_segment = vec![];
let mut found_code = false;
for segment in parent_segment.segments().iter().rev() {
anchor_segment = Some(segment.clone());
if segment.is_type(SyntaxKind::StatementTerminator) {
semi_colon_exist_flag = true;
} else if segment.is_code() {
is_one_line = Self::is_one_line_statement(parent_segment.clone(), segment.clone());
found_code = true;
break;
} else if !segment.is_meta() {
before_segment.push(segment.clone());
}
}
if !found_code {
return None;
}
let semicolon_newline = if is_one_line {
false
} else {
self.multiline_newline
};
if !semi_colon_exist_flag {
return if !semicolon_newline {
let fixes = vec![LintFix::create_after(
anchor_segment.unwrap().clone(),
vec![SegmentBuilder::token(
tables.next_id(),
";",
SyntaxKind::StatementTerminator,
)
.finish()],
None,
)];
Some(LintResult::new(
Some(trigger_segment.unwrap().clone()),
fixes,
None,
None,
None,
))
} else {
let (_before_segment, anchor_segment) = Self::handle_preceding_inline_comments(
Segments::from_vec(before_segment, None),
anchor_segment.unwrap().clone(),
);
let fixes = vec![LintFix::create_after(
anchor_segment.clone(),
vec![
SegmentBuilder::newline(tables.next_id(), "\n"),
SegmentBuilder::token(
tables.next_id(),
";",
SyntaxKind::StatementTerminator,
)
.finish(),
],
None,
)];
Some(LintResult::new(
Some(trigger_segment.unwrap().clone()),
fixes,
None,
None,
None,
))
};
}
None
}
fn get_segment_move_context(
target_segment: ErasedSegment,
parent_segment: ErasedSegment,
) -> SegmentMoveContext {
let reversed_raw_stack =
Segments::from_vec(parent_segment.get_raw_segments(), None).reversed();
let before_code = reversed_raw_stack.select::<fn(&ErasedSegment) -> bool>(
None,
Some(|s| !s.is_code()),
Some(&target_segment),
None,
);
let before_segment = before_code.select(
Some(|segment: &ErasedSegment| !segment.is_meta()),
None,
None,
None,
);
let anchor_segment = if !before_code.is_empty() {
before_code.last().unwrap().clone()
} else {
target_segment.clone()
};
let first_code = reversed_raw_stack
.select(
Some(|s: &ErasedSegment| s.is_code()),
None,
Some(&target_segment),
None,
)
.first()
.cloned();
let is_one_line = first_code.map_or(false, |segment| {
Self::is_one_line_statement(parent_segment, segment.clone())
});
let whitespace_deletions = before_segment.select::<fn(&ErasedSegment) -> bool>(
None,
Some(|segment| segment.is_whitespace()),
None,
None,
);
SegmentMoveContext {
anchor_segment,
is_one_line,
before_segment,
whitespace_deletions,
}
}
}
struct SegmentMoveContext {
anchor_segment: ErasedSegment,
is_one_line: bool,
before_segment: Segments,
whitespace_deletions: Segments,
}
pub fn choose_anchor_segment(
root_segment: &ErasedSegment,
edit_type: EditType,
segment: &ErasedSegment,
filter_meta: bool,
) -> ErasedSegment {
if !matches!(edit_type, EditType::CreateBefore | EditType::CreateAfter) {
return segment.clone();
}
let mut anchor = segment.clone();
let mut child = segment.clone();
let mut path = root_segment
.path_to(segment)
.into_iter()
.map(|it| it.segment)
.collect_vec();
path.reverse();
for seg in path {
if seg.can_start_end_non_code() {
break;
}
let mut children_lists = Vec::new();
if filter_meta {
children_lists.push(
seg.segments()
.iter()
.filter(|child| !child.is_meta())
.cloned()
.collect_vec(),
);
}
children_lists.push(seg.segments().to_vec());
for children in children_lists {
match edit_type {
EditType::CreateBefore if children[0].id() == child.id() => {
unreachable!()
}
EditType::CreateAfter if children.last().unwrap().id() == child.id() => {
anchor = seg.clone();
child = seg;
break;
}
_ => {}
}
}
}
anchor
}