Skip to main content

sqrust_core/
lib.rs

1pub mod config;
2pub use config::Config;
3
4use sqlparser::ast::Statement;
5use sqlparser::dialect::GenericDialect;
6use sqlparser::parser::Parser;
7use std::path::PathBuf;
8
9/// A single lint violation produced by a Rule.
10pub struct Diagnostic {
11    pub rule: &'static str,
12    pub message: String,
13    /// 1-indexed line number
14    pub line: usize,
15    /// 1-indexed column of the violation
16    pub col: usize,
17}
18
19/// All information a Rule needs to check one file.
20pub struct FileContext {
21    pub path: PathBuf,
22    pub source: String,
23    /// Parsed SQL statements. Empty if the file could not be parsed.
24    pub statements: Vec<Statement>,
25    /// Parse error messages, if parsing failed.
26    pub parse_errors: Vec<String>,
27}
28
29impl FileContext {
30    pub fn from_source(source: &str, path: &str) -> Self {
31        let dialect = GenericDialect {};
32        let (statements, parse_errors) = match Parser::parse_sql(&dialect, source) {
33            Ok(stmts) => (stmts, Vec::new()),
34            Err(e) => (Vec::new(), vec![e.to_string()]),
35        };
36        FileContext {
37            path: PathBuf::from(path),
38            source: source.to_string(),
39            statements,
40            parse_errors,
41        }
42    }
43
44    /// Returns (1-indexed line number, line content) for each line.
45    pub fn lines(&self) -> impl Iterator<Item = (usize, &str)> {
46        self.source.lines().enumerate().map(|(i, line)| (i + 1, line))
47    }
48}
49
50/// Every lint rule implements this trait.
51pub trait Rule: Send + Sync {
52    fn name(&self) -> &'static str;
53    fn check(&self, ctx: &FileContext) -> Vec<Diagnostic>;
54    /// Returns the fixed source if this rule supports auto-fix, None otherwise.
55    fn fix(&self, _ctx: &FileContext) -> Option<String> {
56        None
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn valid_sql_populates_statements() {
66        let ctx = FileContext::from_source("SELECT 1; SELECT 2;", "t.sql");
67        assert_eq!(ctx.statements.len(), 2);
68        assert!(ctx.parse_errors.is_empty());
69    }
70
71    #[test]
72    fn invalid_sql_stores_parse_error() {
73        let ctx = FileContext::from_source("SELECT FROM FROM", "t.sql");
74        assert!(ctx.statements.is_empty());
75        assert!(!ctx.parse_errors.is_empty());
76    }
77
78    #[test]
79    fn empty_sql_produces_no_statements_and_no_errors() {
80        let ctx = FileContext::from_source("", "t.sql");
81        assert!(ctx.statements.is_empty());
82        assert!(ctx.parse_errors.is_empty());
83    }
84
85    #[test]
86    fn lines_still_works_after_ast_addition() {
87        let ctx = FileContext::from_source("SELECT 1\nFROM t\n", "t.sql");
88        let lines: Vec<_> = ctx.lines().collect();
89        assert_eq!(lines.len(), 2);
90        assert_eq!(lines[0], (1, "SELECT 1"));
91        assert_eq!(lines[1], (2, "FROM t"));
92    }
93}