use squawk_line_index::{LineEnding, LineIndex, TextSize};
use squawk_linter::{
Edit, Rule, Violation,
ignore::{IGNORE_FILE_TEXT, IGNORE_LINE_TEXT},
};
use squawk_syntax::{Parse, SourceFile, SyntaxKind, SyntaxToken, ast::AstNode};
const UNSUPPORTED_RULES: &[Rule] = &[Rule::UnusedIgnore];
pub(crate) fn ignore_line_edit(
violation: &Violation,
line_index: &LineIndex,
parse: &Parse<SourceFile>,
line_ending: LineEnding,
) -> Option<Edit> {
if UNSUPPORTED_RULES.contains(&violation.code) {
return None;
}
let tree = parse.tree();
let rule_name = violation.code.to_string();
let violation_line = line_index.line_col(violation.text_range.start());
let previous_line = violation_line.line.checked_sub(1)?;
let previous_line_offset = line_index.line(previous_line)?.start();
let previous_line_token = tree
.syntax()
.token_at_offset(previous_line_offset)
.right_biased()?;
match previous_line_token.kind() {
SyntaxKind::COMMENT if is_ignore_comment(&previous_line_token) => {
let (_str, ignore_comment_range, _ignore_kind) =
squawk_linter::ignore::ignore_rule_info(&previous_line_token)?;
Some(Edit::insert(
format!(" {rule_name},"),
ignore_comment_range.start(),
))
}
_ => Some(Edit::insert(
format!("-- {IGNORE_LINE_TEXT} {rule_name}{}", line_ending.as_str()),
line_index.line(violation_line.line)?.start(),
)),
}
}
pub(crate) fn ignore_file_edit(
violation: &Violation,
_line_index: &LineIndex,
_parse: &Parse<SourceFile>,
line_ending: LineEnding,
) -> Option<Edit> {
if UNSUPPORTED_RULES.contains(&violation.code) {
return None;
}
let rule_name = violation.code.to_string();
Some(Edit::insert(
format!("-- {IGNORE_FILE_TEXT} {rule_name}{}", line_ending.as_str()),
TextSize::new(0),
))
}
fn is_ignore_comment(token: &SyntaxToken) -> bool {
assert_eq!(token.kind(), SyntaxKind::COMMENT);
squawk_linter::ignore::ignore_rule_info(token).is_some()
}
#[cfg(test)]
mod test {
use crate::{diagnostic::AssociatedDiagnosticData, lint::lint};
use insta::assert_snapshot;
use squawk_ide::db::{Database, File};
#[test]
fn ignore_line() {
let sql = "
create table a (
a int
);
-- an existing comment that shouldn't get in the way of us adding a new ignore
create table b (
b int
);
-- squawk-ignore prefer-text-field
create table c (
b int
);
";
let ignore_line_edits = lint_sql(sql)
.into_iter()
.flat_map(|x| {
let data = x.data?;
let associated_data: AssociatedDiagnosticData =
serde_json::from_value(data).unwrap();
associated_data.ignore_line_edit
})
.collect::<Vec<_>>();
assert_snapshot!(apply_text_edits(sql, ignore_line_edits), @r"
-- squawk-ignore prefer-robust-stmts
create table a (
-- squawk-ignore prefer-bigint-over-int
a int
);
-- an existing comment that shouldn't get in the way of us adding a new ignore
-- squawk-ignore prefer-robust-stmts
create table b (
-- squawk-ignore prefer-bigint-over-int
b int
);
-- squawk-ignore prefer-robust-stmts, prefer-text-field
create table c (
-- squawk-ignore prefer-bigint-over-int
b int
);
");
}
#[test]
fn ignore_file() {
let sql = "
-- some existing comment
create table a (
a int
);
create table b (
b int
);
create table c (
b int
);
";
let ignore_line_edits = lint_sql(sql)
.into_iter()
.flat_map(|x| {
let data = x.data?;
let associated_data: AssociatedDiagnosticData =
serde_json::from_value(data).unwrap();
associated_data.ignore_file_edit
})
.collect::<Vec<_>>();
assert_snapshot!(apply_text_edits(sql, ignore_line_edits), @r"
-- squawk-ignore-file prefer-bigint-over-int
-- squawk-ignore-file prefer-robust-stmts
-- squawk-ignore-file prefer-bigint-over-int
-- squawk-ignore-file prefer-robust-stmts
-- squawk-ignore-file prefer-bigint-over-int
-- squawk-ignore-file prefer-robust-stmts
-- some existing comment
create table a (
a int
);
create table b (
b int
);
create table c (
b int
);
");
}
fn apply_text_edits(sql: &str, mut edits: Vec<gen_lsp_types::TextEdit>) -> String {
use squawk_line_index::{LineCol, LineIndex};
edits.sort_by(|a, b| {
b.range
.start
.line
.cmp(&a.range.start.line)
.then_with(|| b.range.start.character.cmp(&a.range.start.character))
});
let line_index = LineIndex::new(sql);
let mut result = sql.to_string();
for edit in edits {
let start_offset = line_index.offset(LineCol {
line: edit.range.start.line,
col: edit.range.start.character,
});
let end_offset = line_index.offset(LineCol {
line: edit.range.end.line,
col: edit.range.end.character,
});
let start_byte: usize = start_offset.unwrap_or_default().into();
let end_byte: usize = end_offset.unwrap_or_default().into();
result.replace_range(start_byte..end_byte, &edit.new_text);
}
result
}
fn lint_sql(sql: &str) -> Vec<gen_lsp_types::Diagnostic> {
let db = Database::default();
let file = File::new(&db, sql.to_owned().into());
lint(&db, file)
}
fn ignore_line_edits_with_line_ending(line_ending: &str) -> String {
use squawk_line_index::{LineIndex, find_newline};
use squawk_linter::Linter;
use squawk_syntax::SourceFile;
let sql = [
"create table a (",
" a int",
");",
"",
"-- squawk-ignore prefer-text-field",
"create table b (",
" b text",
");",
"",
]
.join(line_ending);
let parse = SourceFile::parse(&sql);
let line_index = LineIndex::new(&sql);
let detected = find_newline(&sql)
.map(|(_, ending)| ending)
.unwrap_or_default();
let mut edits = Linter::with_default_rules()
.lint(&parse, &sql)
.iter()
.filter_map(|v| super::ignore_line_edit(v, &line_index, &parse, detected))
.collect::<Vec<_>>();
edits.sort_by_key(|e| std::cmp::Reverse(e.text_range.start()));
let mut result = sql.clone();
for edit in edits {
let start: usize = edit.text_range.start().into();
let end: usize = edit.text_range.end().into();
result.replace_range(start..end, &edit.text.unwrap_or_default());
}
result.replace('\r', "<CR>")
}
#[test]
fn ignore_line_with_lf_line_endings() {
assert_snapshot!(ignore_line_edits_with_line_ending("\n"), @"
create table a (
-- squawk-ignore prefer-bigint-over-int
a int
);
-- squawk-ignore prefer-robust-stmts, prefer-text-field
create table b (
b text
);
");
}
#[test]
fn ignore_line_with_crlf_line_endings() {
assert_snapshot!(ignore_line_edits_with_line_ending("\r\n"), @"
create table a (<CR>
-- squawk-ignore prefer-bigint-over-int<CR>
a int<CR>
);<CR>
<CR>
-- squawk-ignore prefer-robust-stmts, prefer-text-field<CR>
create table b (<CR>
b text<CR>
);<CR>
");
}
#[test]
fn ignore_line_with_cr_line_endings() {
assert_snapshot!(ignore_line_edits_with_line_ending("\r"), @"create table a (<CR>-- squawk-ignore prefer-bigint-over-int<CR> a int<CR>);<CR><CR>-- squawk-ignore prefer-robust-stmts, prefer-text-field<CR>create table b (<CR> b text<CR>);<CR>");
}
}