use rigsql_core::SegmentType;
use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
use crate::violation::{LintViolation, SourceEdit};
#[derive(Debug, Default)]
pub struct RuleLT09;
impl Rule for RuleLT09 {
fn code(&self) -> &'static str {
"LT09"
}
fn name(&self) -> &'static str {
"layout.select_targets"
}
fn description(&self) -> &'static str {
"Select targets should be on a new line unless there is only one."
}
fn explanation(&self) -> &'static str {
"When a SELECT has multiple columns, each column should be on its own line. \
This makes diffs cleaner and improves readability. A single column can stay \
on the same line as SELECT."
}
fn groups(&self) -> &[RuleGroup] {
&[RuleGroup::Layout]
}
fn is_fixable(&self) -> bool {
true
}
fn crawl_type(&self) -> CrawlType {
CrawlType::Segment(vec![SegmentType::SelectClause])
}
fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
let children = ctx.segment.children();
let targets: Vec<_> = children
.iter()
.filter(|c| {
let st = c.segment_type();
!st.is_trivia() && st != SegmentType::Keyword && st != SegmentType::Comma
})
.collect();
if targets.len() <= 1 {
return vec![];
}
let has_newline_between_targets = children
.iter()
.any(|c| c.segment_type() == SegmentType::Newline);
if !has_newline_between_targets {
let mut fixes = Vec::new();
let indent = " ";
for (i, child) in children.iter().enumerate() {
if child.segment_type() == SegmentType::Keyword && i + 1 < children.len() {
let next = &children[i + 1];
if next.segment_type() == SegmentType::Whitespace {
fixes.push(SourceEdit::replace(next.span(), format!("\n{}", indent)));
}
}
if child.segment_type() == SegmentType::Comma && i + 1 < children.len() {
let next = &children[i + 1];
if next.segment_type() == SegmentType::Whitespace {
fixes.push(SourceEdit::replace(next.span(), format!("\n{}", indent)));
} else {
fixes.push(SourceEdit::insert(
child.span().end,
format!("\n{}", indent),
));
}
}
}
return vec![LintViolation::with_fix_and_msg_key(
self.code(),
"Select targets should be on separate lines.",
ctx.segment.span(),
fixes,
"rules.LT09.msg",
vec![],
)];
}
vec![]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::lint_sql;
#[test]
fn test_lt09_flags_multiple_targets_single_line() {
let violations = lint_sql("SELECT a, b, c FROM t", RuleLT09);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule_code, "LT09");
assert!(!violations[0].fixes.is_empty());
}
#[test]
fn test_lt09_accepts_single_target() {
let violations = lint_sql("SELECT a FROM t", RuleLT09);
assert_eq!(violations.len(), 0);
}
}