Skip to main content

bwq_lint/
lib.rs

1pub mod ast;
2pub mod error;
3pub mod lexer;
4pub mod lsp;
5pub mod parser;
6pub mod validation;
7pub mod validator;
8
9use error::{LintError, LintReport, LintResult};
10use lexer::Lexer;
11use parser::Parser;
12use validator::Validator;
13
14pub use lsp::LspServer;
15
16pub struct BrandwatchLinter {
17    validator: Validator,
18}
19
20impl BrandwatchLinter {
21    pub fn new() -> Self {
22        Self {
23            validator: Validator::new(),
24        }
25    }
26
27    pub fn lint(&mut self, query: &str) -> LintResult<LintReport> {
28        let mut lexer = Lexer::new(query);
29        let tokens = lexer.tokenize()?;
30
31        let mut parser = Parser::new(tokens);
32        let parse_result = parser.parse()?;
33
34        let mut report = self.validator.validate(&parse_result.query);
35        report.warnings.extend(parse_result.warnings);
36
37        Ok(report)
38    }
39
40    pub fn is_valid(&mut self, query: &str) -> bool {
41        match self.lint(query) {
42            Ok(report) => !report.has_errors(),
43            Err(_) => false,
44        }
45    }
46
47    pub fn analyze(&mut self, query: &str) -> AnalysisResult {
48        if query.trim().is_empty() {
49            return AnalysisResult {
50                is_valid: true,
51                errors: Vec::new(),
52                warnings: Vec::new(),
53                query: Some(query.to_string()),
54            };
55        }
56
57        match self.lint(query) {
58            Ok(report) => AnalysisResult {
59                is_valid: !report.has_errors(),
60                errors: report.errors,
61                warnings: report.warnings,
62                query: Some(query.to_string()),
63            },
64            Err(error) => AnalysisResult {
65                is_valid: false,
66                errors: vec![error],
67                warnings: vec![],
68                query: Some(query.to_string()),
69            },
70        }
71    }
72}
73
74impl Default for BrandwatchLinter {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80#[derive(Debug, Clone)]
81pub struct AnalysisResult {
82    pub is_valid: bool,
83    pub errors: Vec<LintError>,
84    pub warnings: Vec<error::LintWarning>,
85    pub query: Option<String>,
86}
87
88impl AnalysisResult {
89    pub fn has_issues(&self) -> bool {
90        !self.errors.is_empty() || !self.warnings.is_empty()
91    }
92
93    pub fn summary(&self) -> String {
94        if self.is_valid && self.warnings.is_empty() {
95            "Query is valid with no issues".to_string()
96        } else {
97            let error_count = self.errors.len();
98            let warning_count = self.warnings.len();
99
100            match (error_count, warning_count) {
101                (0, 0) => "Query is valid with no issues".to_string(),
102                (0, w) => format!(
103                    "Query is valid with {} warning{}",
104                    w,
105                    if w == 1 { "" } else { "s" }
106                ),
107                (e, 0) => format!("Query has {} error{}", e, if e == 1 { "" } else { "s" }),
108                (e, w) => format!(
109                    "Query has {} error{} and {} warning{}",
110                    e,
111                    if e == 1 { "" } else { "s" },
112                    w,
113                    if w == 1 { "" } else { "s" }
114                ),
115            }
116        }
117    }
118
119    pub fn format_issues(&self) -> String {
120        let mut output = String::new();
121
122        if !self.errors.is_empty() {
123            output.push_str("Errors:\n");
124            for (i, error) in self.errors.iter().enumerate() {
125                output.push_str(&format!("  {}. {}\n", i + 1, error));
126            }
127        }
128
129        if !self.warnings.is_empty() {
130            if !output.is_empty() {
131                output.push('\n');
132            }
133            output.push_str("Warnings:\n");
134            for (i, warning) in self.warnings.iter().enumerate() {
135                output.push_str(&format!("  {}. {:?}\n", i + 1, warning));
136            }
137        }
138
139        output
140    }
141}
142
143pub fn lint_query(query: &str) -> LintResult<LintReport> {
144    let mut linter = BrandwatchLinter::new();
145    linter.lint(query)
146}
147
148pub fn is_valid_query(query: &str) -> bool {
149    let mut linter = BrandwatchLinter::new();
150    linter.is_valid(query)
151}
152
153pub fn analyze_query(query: &str) -> AnalysisResult {
154    let mut linter = BrandwatchLinter::new();
155    linter.analyze(query)
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_basic_linting() {
164        let mut linter = BrandwatchLinter::new();
165        let report = linter.lint("apple AND juice").unwrap();
166        assert!(!report.has_errors());
167    }
168
169    #[test]
170    fn test_invalid_query() {
171        let mut linter = BrandwatchLinter::new();
172        let report = linter.lint("rating:6").unwrap();
173        assert!(report.has_errors());
174    }
175
176    #[test]
177    fn test_convenience_functions() {
178        assert!(is_valid_query("apple AND juice"));
179        assert!(!is_valid_query("*invalid"));
180
181        let analysis = analyze_query("apple AND juice");
182        assert!(analysis.is_valid);
183        assert!(!analysis.has_issues());
184    }
185
186    #[test]
187    fn test_complex_query() {
188        let query = r#"(apple OR orange) AND "fruit juice" NOT bitter"#;
189        let mut linter = BrandwatchLinter::new();
190        let report = linter.lint(query).unwrap();
191        assert!(!report.has_errors());
192    }
193
194    #[test]
195    fn test_field_query() {
196        let query = r#"title:"apple juice" AND site:twitter.com"#;
197        let mut linter = BrandwatchLinter::new();
198        let report = linter.lint(query).unwrap();
199        assert!(!report.has_errors());
200    }
201
202    #[test]
203    fn test_proximity_query() {
204        let mut linter = BrandwatchLinter::new();
205
206        let query1 = r#"apple NEAR/3 juice"#;
207        let report1 = linter.lint(query1).unwrap();
208        assert!(!report1.has_errors());
209
210        let query2 = r#""apple juice"~5"#;
211        let report2 = linter.lint(query2).unwrap();
212        assert!(!report2.has_errors());
213    }
214
215    #[test]
216    fn test_analysis_result_summary() {
217        let analysis = analyze_query("apple AND juice");
218        assert_eq!(analysis.summary(), "Query is valid with no issues");
219
220        let analysis = analyze_query("*invalid");
221        assert!(analysis.summary().contains("error"));
222    }
223}