Skip to main content

sql_dialect_fmt_lsp/
lint.rs

1//! Thin LSP adapter over the `sql-dialect-fmt-lint` rule engine.
2//!
3//! The rules, options, and suppression comments live in `sql_dialect_fmt_lint`; this module only
4//! converts its byte-ranged [`sql_dialect_fmt_lint::LintDiagnostic`]s into `lsp_types::Diagnostic`s
5//! using the negotiated position encoding, and re-exports the types LSP consumers already use.
6
7use lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Range};
8use sql_dialect_fmt_lint::LintSeverity;
9use sql_dialect_fmt_parser::Dialect;
10use sql_dialect_fmt_text::LineIndex;
11
12use crate::{lsp_position, PositionEncoding};
13
14pub use sql_dialect_fmt_lint::{LintCode, LintOptions};
15
16/// The [`LintCode`] carried by an LSP diagnostic, when it is one of ours.
17pub fn diagnostic_lint_code(diagnostic: &Diagnostic) -> Option<LintCode> {
18    match diagnostic.code.as_ref()? {
19        NumberOrString::String(value) => LintCode::from_code(value),
20        NumberOrString::Number(_) => None,
21    }
22}
23
24pub(crate) fn diagnostics_with_encoding(
25    text: &str,
26    dialect: Dialect,
27    index: &LineIndex<'_>,
28    options: LintOptions,
29    encoding: PositionEncoding,
30) -> Vec<Diagnostic> {
31    sql_dialect_fmt_lint::lint_with_dialect(text, dialect, options)
32        .into_iter()
33        .map(|finding| Diagnostic {
34            range: Range::new(
35                lsp_position(index, finding.range.start, encoding),
36                lsp_position(index, finding.range.end, encoding),
37            ),
38            severity: Some(match finding.severity {
39                LintSeverity::Warning => DiagnosticSeverity::WARNING,
40                LintSeverity::Error => DiagnosticSeverity::ERROR,
41            }),
42            code: Some(NumberOrString::String(finding.code.as_str().to_string())),
43            source: Some("sql-dialect-fmt".to_string()),
44            message: finding.message,
45            ..Default::default()
46        })
47        .collect()
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn lint_findings_convert_to_lsp_diagnostics() {
56        let text = "SELECT * FROM t;";
57        let index = LineIndex::new(text);
58        let diagnostics = diagnostics_with_encoding(
59            text,
60            Dialect::Snowflake,
61            &index,
62            LintOptions::default(),
63            PositionEncoding::Utf16,
64        );
65        let wildcard = diagnostics
66            .iter()
67            .find(|diagnostic| diagnostic_lint_code(diagnostic) == Some(LintCode::SelectWildcard))
68            .expect("SDF001 diagnostic");
69        assert_eq!(wildcard.severity, Some(DiagnosticSeverity::WARNING));
70        assert_eq!(wildcard.source.as_deref(), Some("sql-dialect-fmt"));
71        let col = text.find('*').unwrap() as u32;
72        assert_eq!(wildcard.range.start, lsp_types::Position::new(0, col));
73        assert_eq!(wildcard.range.end, lsp_types::Position::new(0, col + 1));
74    }
75
76    #[test]
77    fn suppression_comments_apply_before_conversion() {
78        let text = "-- sql-dialect-fmt: disable-next-line SDF001\nSELECT * FROM t;";
79        let index = LineIndex::new(text);
80        assert!(diagnostics_with_encoding(
81            text,
82            Dialect::Snowflake,
83            &index,
84            LintOptions::default(),
85            PositionEncoding::Utf16,
86        )
87        .is_empty());
88    }
89}