From e0adc207ddff117a1fe24465581f79b979186b5f Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Wed, 20 Aug 2025 20:39:58 -0400 Subject: [PATCH 1/2] server: add quick fixes to ignore rule violations --- Cargo.lock | 1 + crates/squawk_linter/src/ignore.rs | 8 +- crates/squawk_linter/src/lib.rs | 4 +- crates/squawk_server/Cargo.toml | 1 + crates/squawk_server/src/diagnostic.rs | 19 +++ crates/squawk_server/src/ignore.rs | 145 +++++++++++++++++ crates/squawk_server/src/lib.rs | 216 +++++++++---------------- crates/squawk_server/src/lint.rs | 113 +++++++++++++ 8 files changed, 362 insertions(+), 145 deletions(-) create mode 100644 crates/squawk_server/src/diagnostic.rs create mode 100644 crates/squawk_server/src/ignore.rs create mode 100644 crates/squawk_server/src/lint.rs diff --git a/Cargo.lock b/Cargo.lock index 9f86fef9..475f17ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2673,6 +2673,7 @@ name = "squawk_server" version = "0.0.0" dependencies = [ "anyhow", + "insta", "line-index", "log", "lsp-server", diff --git a/crates/squawk_linter/src/ignore.rs b/crates/squawk_linter/src/ignore.rs index d8837da1..691140d3 100644 --- a/crates/squawk_linter/src/ignore.rs +++ b/crates/squawk_linter/src/ignore.rs @@ -42,10 +42,10 @@ fn comment_body(token: &SyntaxToken) -> Option<(&str, TextRange)> { } // TODO: maybe in a future version we can rename this to squawk-ignore-line -const IGNORE_LINE_TEXT: &str = "squawk-ignore"; -const IGNORE_FILE_TEXT: &str = "squawk-ignore-file"; +pub const IGNORE_LINE_TEXT: &str = "squawk-ignore"; +pub const IGNORE_FILE_TEXT: &str = "squawk-ignore-file"; -fn ignore_rule_names(token: &SyntaxToken) -> Option<(&str, TextRange, IgnoreKind)> { +pub fn ignore_rule_info(token: &SyntaxToken) -> Option<(&str, TextRange, IgnoreKind)> { if let Some((comment_body, range)) = comment_body(token) { let without_start = comment_body.trim_start(); let trim_start_size = comment_body.len() - without_start.len(); @@ -74,7 +74,7 @@ pub(crate) fn find_ignores(ctx: &mut Linter, file: &SyntaxNode) { rowan::WalkEvent::Enter(NodeOrToken::Token(token)) if token.kind() == SyntaxKind::COMMENT => { - if let Some((rule_names, range, kind)) = ignore_rule_names(&token) { + if let Some((rule_names, range, kind)) = ignore_rule_info(&token) { let mut set = HashSet::new(); let mut offset = 0usize; diff --git a/crates/squawk_linter/src/lib.rs b/crates/squawk_linter/src/lib.rs index 29316798..7909b599 100644 --- a/crates/squawk_linter/src/lib.rs +++ b/crates/squawk_linter/src/lib.rs @@ -15,7 +15,7 @@ use squawk_syntax::{Parse, SourceFile}; pub use version::Version; -mod ignore; +pub mod ignore; mod ignore_index; mod version; mod visitors; @@ -250,7 +250,7 @@ pub struct Edit { pub text: Option, } impl Edit { - fn insert>(text: T, at: TextSize) -> Self { + pub fn insert>(text: T, at: TextSize) -> Self { Self { text_range: TextRange::new(at, at), text: Some(text.into()), diff --git a/crates/squawk_server/Cargo.toml b/crates/squawk_server/Cargo.toml index d1303f96..f96cd790 100644 --- a/crates/squawk_server/Cargo.toml +++ b/crates/squawk_server/Cargo.toml @@ -19,6 +19,7 @@ squawk_lexer.workspace = true squawk_linter.workspace = true squawk_syntax.workspace = true line-index.workspace = true +insta.workspace = true [lints] workspace = true diff --git a/crates/squawk_server/src/diagnostic.rs b/crates/squawk_server/src/diagnostic.rs new file mode 100644 index 00000000..de5ae2ce --- /dev/null +++ b/crates/squawk_server/src/diagnostic.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +pub(crate) const DIAGNOSTIC_NAME: &str = "squawk"; + +// Based on Ruff's setup for LSP diagnostic edits +// see: https://github.com/astral-sh/ruff/blob/1a368b0bf97c3d0246390679166bbd2d589acf39/crates/ruff_server/src/lint.rs#L31 +/// This is serialized on the diagnostic `data` field. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub(crate) struct AssociatedDiagnosticData { + /// The message describing what the fix does, if it exists, or the diagnostic name otherwise. + pub(crate) title: String, + /// Edits to fix the diagnostic. If this is empty, a fix + /// does not exist. + pub(crate) edits: Vec, + /// Edit to ignore the rule the line + pub(crate) ignore_line_edit: Option, + /// Edit to ignore the rule for the file + pub(crate) ignore_file_edit: Option, +} diff --git a/crates/squawk_server/src/ignore.rs b/crates/squawk_server/src/ignore.rs new file mode 100644 index 00000000..7c91cd5b --- /dev/null +++ b/crates/squawk_server/src/ignore.rs @@ -0,0 +1,145 @@ +use line_index::{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, +) -> Option { + 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, range, _ignore_kind) = + squawk_linter::ignore::ignore_rule_info(&previous_line_token)?; + Some(Edit::insert(format!(" {rule_name},"), range.start())) + } + _ => Some(Edit::insert( + format!("-- {IGNORE_LINE_TEXT} {rule_name}\n"), + violation.text_range.start(), + )), + } +} + +pub(crate) fn ignore_file_edit( + violation: &Violation, + _line_index: &LineIndex, + _parse: &Parse, +) -> Option { + if UNSUPPORTED_RULES.contains(&violation.code) { + return None; + } + let rule_name = violation.code.to_string(); + Some(Edit::insert( + format!("-- {IGNORE_FILE_TEXT} {rule_name}\n"), + 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}; + + #[test] + fn ignore_line_edit_works() { + 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) + .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::>(); + insta::assert_snapshot!(apply_text_edits(sql, ignore_line_edits), @r" + -- squawk-ignore prefer-robust-stmts + create table a ( + 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 ( + b int + ); + + -- squawk-ignore prefer-robust-stmts, prefer-text-field + create table c ( + b int + ); + "); + } + + fn apply_text_edits(sql: &str, mut edits: Vec) -> String { + use line_index::{LineCol, LineIndex}; + + // Sort edits by position (reverse order to apply from end to start) + 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 { + // Convert LSP positions to byte offsets + 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 + } +} diff --git a/crates/squawk_server/src/lib.rs b/crates/squawk_server/src/lib.rs index 1de040a8..30c8da99 100644 --- a/crates/squawk_server/src/lib.rs +++ b/crates/squawk_server/src/lib.rs @@ -1,25 +1,28 @@ use anyhow::{Context, Result}; -use line_index::LineIndex; use log::info; use lsp_server::{Connection, Message, Notification, Response}; use lsp_types::{ CodeAction, CodeActionKind, CodeActionOptions, CodeActionOrCommand, CodeActionParams, - CodeActionProviderCapability, CodeActionResponse, CodeDescription, Command, Diagnostic, - DiagnosticSeverity, DidChangeTextDocumentParams, DidCloseTextDocumentParams, - DidOpenTextDocumentParams, GotoDefinitionParams, GotoDefinitionResponse, InitializeParams, - Location, Position, PublishDiagnosticsParams, Range, ServerCapabilities, - TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url, WorkDoneProgressOptions, - WorkspaceEdit, + CodeActionProviderCapability, CodeActionResponse, Command, Diagnostic, + DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, + GotoDefinitionParams, GotoDefinitionResponse, InitializeParams, Location, Position, + PublishDiagnosticsParams, Range, ServerCapabilities, TextDocumentSyncCapability, + TextDocumentSyncKind, Url, WorkDoneProgressOptions, WorkspaceEdit, notification::{ DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Notification as _, PublishDiagnostics, }, request::{CodeActionRequest, GotoDefinition, Request}, }; -use serde::{Deserialize, Serialize}; -use squawk_linter::Linter; use squawk_syntax::{Parse, SourceFile}; use std::collections::HashMap; + +use diagnostic::DIAGNOSTIC_NAME; + +use crate::diagnostic::AssociatedDiagnosticData; +mod diagnostic; +mod ignore; +mod lint; mod lsp_utils; struct DocumentState { @@ -142,8 +145,6 @@ fn handle_goto_definition(connection: &Connection, req: lsp_server::Request) -> Ok(()) } -const DIAGNOSTIC_NAME: &str = "squawk"; - fn handle_code_action( connection: &Connection, req: lsp_server::Request, @@ -160,39 +161,80 @@ fn handle_code_action( .into_iter() .filter(|diagnostic| diagnostic.source.as_deref() == Some(DIAGNOSTIC_NAME)) { - if let Some(code) = diagnostic.code.as_ref() { - let rule_name = match code { - lsp_types::NumberOrString::String(s) => s.clone(), - lsp_types::NumberOrString::Number(n) => n.to_string(), - }; + let Some(rule_name) = diagnostic.code.as_ref().map(|x| match x { + lsp_types::NumberOrString::String(s) => s.clone(), + lsp_types::NumberOrString::Number(n) => n.to_string(), + }) else { + continue; + }; + let Some(data) = diagnostic.data.take() else { + continue; + }; - let title = format!("Show documentation for {}", rule_name); + let associated_data: AssociatedDiagnosticData = + serde_json::from_value(data).context("deserializing diagnostic data")?; - let documentation_action = CodeAction { - title: title.clone(), + if let Some(ignore_line_edit) = associated_data.ignore_line_edit { + let disable_line_action = CodeAction { + title: format!("Disable {rule_name} for this line"), kind: Some(CodeActionKind::QUICKFIX), diagnostics: Some(vec![diagnostic.clone()]), - edit: None, - command: Some(Command { - title, - command: "vscode.open".to_string(), - arguments: Some(vec![serde_json::to_value(format!( - "https://squawkhq.com/docs/{}", - rule_name - ))?]), + edit: Some(WorkspaceEdit { + changes: Some({ + let mut changes = HashMap::new(); + changes.insert(uri.clone(), vec![ignore_line_edit]); + changes + }), + ..Default::default() }), + command: None, is_preferred: Some(false), disabled: None, data: None, }; - - actions.push(CodeActionOrCommand::CodeAction(documentation_action)); + actions.push(CodeActionOrCommand::CodeAction(disable_line_action)); + } + if let Some(ignore_file_edit) = associated_data.ignore_file_edit { + let disable_file_action = CodeAction { + title: format!("Disable {rule_name} for the entire file"), + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: Some(vec![diagnostic.clone()]), + edit: Some(WorkspaceEdit { + changes: Some({ + let mut changes = HashMap::new(); + changes.insert(uri.clone(), vec![ignore_file_edit]); + changes + }), + ..Default::default() + }), + command: None, + is_preferred: Some(false), + disabled: None, + data: None, + }; + actions.push(CodeActionOrCommand::CodeAction(disable_file_action)); } - if let Some(data) = diagnostic.data.take() { - let associated_data: AssociatedDiagnosticData = - serde_json::from_value(data).context("deserializing diagnostic data")?; + let title = format!("Show documentation for {rule_name}"); + let documentation_action = CodeAction { + title: title.clone(), + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: Some(vec![diagnostic.clone()]), + edit: None, + command: Some(Command { + title, + command: "vscode.open".to_string(), + arguments: Some(vec![serde_json::to_value(format!( + "https://squawkhq.com/docs/{rule_name}" + ))?]), + }), + is_preferred: Some(false), + disabled: None, + data: None, + }; + actions.push(CodeActionOrCommand::CodeAction(documentation_action)); + if !associated_data.title.is_empty() && !associated_data.edits.is_empty() { let fix_action = CodeAction { title: associated_data.title, kind: Some(CodeActionKind::QUICKFIX), @@ -210,9 +252,8 @@ fn handle_code_action( disabled: None, data: None, }; - actions.push(CodeActionOrCommand::CodeAction(fix_action)); - }; + } } let result: CodeActionResponse = actions; @@ -264,7 +305,7 @@ fn handle_did_open( let content = documents.get(&uri).map_or("", |doc| &doc.content); // TODO: we need a better setup for "run func when input changed" - let diagnostics = lint(content); + let diagnostics = lint::lint(content); publish_diagnostics(connection, uri, version, diagnostics)?; Ok(()) @@ -287,7 +328,7 @@ fn handle_did_change( lsp_utils::apply_incremental_changes(&doc_state.content, params.content_changes); doc_state.version = version; - let diagnostics = lint(&doc_state.content); + let diagnostics = lint::lint(&doc_state.content); publish_diagnostics(connection, uri, version, diagnostics)?; Ok(()) @@ -321,109 +362,6 @@ fn handle_did_close( Ok(()) } -// Based on Ruff's setup for LSP diagnostic edits -// see: https://github.com/astral-sh/ruff/blob/1a368b0bf97c3d0246390679166bbd2d589acf39/crates/ruff_server/src/lint.rs#L31 -/// This is serialized on the diagnostic `data` field. -#[derive(Serialize, Deserialize, Debug, Clone)] -struct AssociatedDiagnosticData { - /// The message describing what the fix does, if it exists, or the diagnostic name otherwise. - title: String, - /// Edits to fix the diagnostic. If this is empty, a fix - /// does not exist. - edits: Vec, -} - -fn lint(content: &str) -> Vec { - let parse: Parse = SourceFile::parse(content); - let parse_errors = parse.errors(); - let mut linter = Linter::with_all_rules(); - let violations = linter.lint(&parse, content); - let line_index = LineIndex::new(content); - - let mut diagnostics = Vec::with_capacity(violations.len() + parse_errors.len()); - - for error in parse_errors { - let range_start = error.range().start(); - let range_end = error.range().end(); - let start_line_col = line_index.line_col(range_start); - let mut end_line_col = line_index.line_col(range_end); - - if start_line_col.line == end_line_col.line && start_line_col.col == end_line_col.col { - end_line_col.col += 1; - } - - let diagnostic = Diagnostic { - range: Range::new( - Position::new(start_line_col.line, start_line_col.col), - Position::new(end_line_col.line, end_line_col.col), - ), - severity: Some(DiagnosticSeverity::ERROR), - code: Some(lsp_types::NumberOrString::String( - "syntax-error".to_string(), - )), - code_description: Some(CodeDescription { - href: Url::parse("https://squawkhq.com/docs/syntax-error").unwrap(), - }), - source: Some(DIAGNOSTIC_NAME.to_string()), - message: error.message().to_string(), - ..Default::default() - }; - diagnostics.push(diagnostic); - } - - for violation in violations { - let range_start = violation.text_range.start(); - let range_end = violation.text_range.end(); - let start_line_col = line_index.line_col(range_start); - let mut end_line_col = line_index.line_col(range_end); - - if start_line_col.line == end_line_col.line && start_line_col.col == end_line_col.col { - end_line_col.col += 1; - } - - let data = if let Some(fix) = violation.fix { - Some(AssociatedDiagnosticData { - title: fix.title, - edits: fix - .edits - .into_iter() - .filter_map(|x| { - let start_line = line_index.try_line_col(x.text_range.start())?; - let end_line = line_index.try_line_col(x.text_range.end())?; - let range = Range::new( - Position::new(start_line.line, start_line.col), - Position::new(end_line.line, end_line.col), - ); - Some(TextEdit::new(range, x.text.unwrap_or_default())) - }) - .collect(), - }) - } else { - None - }; - - let diagnostic = Diagnostic { - range: Range::new( - Position::new(start_line_col.line, start_line_col.col), - Position::new(end_line_col.line, end_line_col.col), - ), - severity: Some(DiagnosticSeverity::WARNING), - code: Some(lsp_types::NumberOrString::String( - violation.code.to_string(), - )), - code_description: Some(CodeDescription { - href: Url::parse(&format!("https://squawkhq.com/docs/{}", violation.code)).unwrap(), - }), - source: Some(DIAGNOSTIC_NAME.to_string()), - message: violation.message, - data: data.map(|d| serde_json::to_value(d).unwrap()), - ..Default::default() - }; - diagnostics.push(diagnostic); - } - diagnostics -} - #[derive(serde::Deserialize)] struct SyntaxTreeParams { #[serde(rename = "textDocument")] diff --git a/crates/squawk_server/src/lint.rs b/crates/squawk_server/src/lint.rs new file mode 100644 index 00000000..887da3f5 --- /dev/null +++ b/crates/squawk_server/src/lint.rs @@ -0,0 +1,113 @@ +use line_index::LineIndex; +use lsp_types::{CodeDescription, Diagnostic, DiagnosticSeverity, Position, Range, TextEdit, Url}; +use squawk_linter::{Edit, Linter}; +use squawk_syntax::{Parse, SourceFile}; + +use crate::{ + DIAGNOSTIC_NAME, + diagnostic::AssociatedDiagnosticData, + ignore::{ignore_file_edit, ignore_line_edit}, +}; + +fn to_text_edit(edit: Edit, line_index: &LineIndex) -> Option { + let start_line = line_index.try_line_col(edit.text_range.start())?; + let end_line = line_index.try_line_col(edit.text_range.end())?; + let range = Range::new( + Position::new(start_line.line, start_line.col), + Position::new(end_line.line, end_line.col), + ); + Some(TextEdit::new(range, edit.text.unwrap_or_default())) +} + +pub(crate) fn lint(content: &str) -> Vec { + let parse: Parse = SourceFile::parse(content); + let parse_errors = parse.errors(); + let mut linter = Linter::with_all_rules(); + let violations = linter.lint(&parse, content); + let line_index = LineIndex::new(content); + + let mut diagnostics = Vec::with_capacity(violations.len() + parse_errors.len()); + + for error in parse_errors { + let range_start = error.range().start(); + let range_end = error.range().end(); + let start_line_col = line_index.line_col(range_start); + let mut end_line_col = line_index.line_col(range_end); + + if start_line_col.line == end_line_col.line && start_line_col.col == end_line_col.col { + end_line_col.col += 1; + } + + let diagnostic = Diagnostic { + range: Range::new( + Position::new(start_line_col.line, start_line_col.col), + Position::new(end_line_col.line, end_line_col.col), + ), + severity: Some(DiagnosticSeverity::ERROR), + code: Some(lsp_types::NumberOrString::String( + "syntax-error".to_string(), + )), + code_description: Some(CodeDescription { + href: Url::parse("https://squawkhq.com/docs/syntax-error").unwrap(), + }), + source: Some(DIAGNOSTIC_NAME.to_string()), + message: error.message().to_string(), + ..Default::default() + }; + diagnostics.push(diagnostic); + } + + for violation in violations { + let range_start = violation.text_range.start(); + let range_end = violation.text_range.end(); + let start_line_col = line_index.line_col(range_start); + let mut end_line_col = line_index.line_col(range_end); + + if start_line_col.line == end_line_col.line && start_line_col.col == end_line_col.col { + end_line_col.col += 1; + } + + let ignore_line_edit = ignore_line_edit(&violation, &line_index, &parse) + .and_then(|e| to_text_edit(e, &line_index)); + let ignore_file_edit = ignore_file_edit(&violation, &line_index, &parse) + .and_then(|e| to_text_edit(e, &line_index)); + + let (title, fix_edits) = if let Some(fix) = violation.fix { + (fix.title, fix.edits) + } else { + ("".to_string(), vec![]) + }; + + let edits = fix_edits + .into_iter() + .filter_map(|x| to_text_edit(x, &line_index)) + .collect(); + + let data = AssociatedDiagnosticData { + title, + edits, + ignore_line_edit, + ignore_file_edit, + }; + + let diagnostic = Diagnostic { + range: Range::new( + Position::new(start_line_col.line, start_line_col.col), + Position::new(end_line_col.line, end_line_col.col), + ), + severity: Some(DiagnosticSeverity::WARNING), + code: Some(lsp_types::NumberOrString::String( + violation.code.to_string(), + )), + code_description: Some(CodeDescription { + href: Url::parse(&format!("https://squawkhq.com/docs/{}", violation.code)).unwrap(), + }), + source: Some(DIAGNOSTIC_NAME.to_string()), + message: violation.message, + data: Some(serde_json::to_value(data).unwrap()), + ..Default::default() + }; + diagnostics.push(diagnostic); + } + diagnostics +} From a90ff960748a778ec1eab1ddf6c95db0e7c403d9 Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Wed, 20 Aug 2025 20:47:37 -0400 Subject: [PATCH 2/2] add test --- crates/squawk_server/src/ignore.rs | 47 +++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/crates/squawk_server/src/ignore.rs b/crates/squawk_server/src/ignore.rs index 7c91cd5b..40bac135 100644 --- a/crates/squawk_server/src/ignore.rs +++ b/crates/squawk_server/src/ignore.rs @@ -64,7 +64,7 @@ mod test { use crate::{diagnostic::AssociatedDiagnosticData, lint::lint}; #[test] - fn ignore_line_edit_works() { + fn ignore_line() { let sql = " create table a ( a int @@ -108,6 +108,51 @@ create table c ( "); } + #[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) + .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::>(); + insta::assert_snapshot!(apply_text_edits(sql, ignore_line_edits), @r" + -- squawk-ignore-file prefer-robust-stmts + -- squawk-ignore-file prefer-robust-stmts + -- 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) -> String { use line_index::{LineCol, LineIndex};