Skip to main content

bwq_lint/validation/rules/
performance_rules.rs

1use crate::ast::*;
2use crate::error::{LintError, LintWarning};
3use crate::validation::{ValidationContext, ValidationResult, ValidationRule};
4
5// Wildcard performance validation rule
6pub struct WildcardPerformanceRule;
7
8impl ValidationRule for WildcardPerformanceRule {
9    fn name(&self) -> &'static str {
10        "wildcard-performance"
11    }
12
13    fn validate(&self, expr: &Expression, _ctx: &ValidationContext) -> ValidationResult {
14        match expr {
15            Expression::Term { term, span } => match term {
16                Term::Wildcard { value } => {
17                    let mut result = ValidationResult::new();
18
19                    if value.starts_with('*') {
20                        result
21                            .errors
22                            .push(LintError::InvalidWildcardPlacement { span: span.clone() });
23                    }
24
25                    let parts: Vec<&str> = value.split('*').collect();
26                    if let Some(first_part) = parts.first() {
27                        if !first_part.is_empty() {
28                            if first_part.len() == 1 {
29                                result.errors.push(LintError::ValidationError {
30                                        span: span.clone(),
31                                        message: "This wildcard matches too many unique terms. Please make it more specific.".to_string(),
32                                    });
33                            } else if first_part.len() == 2 {
34                                result.warnings.push(LintWarning::PerformanceWarning {
35                                    span: span.clone(),
36                                    message: "Short wildcard terms may impact performance"
37                                        .to_string(),
38                                });
39                            }
40                        }
41                    }
42
43                    result
44                }
45                Term::Replacement { value } => {
46                    let question_count = value.chars().filter(|&c| c == '?').count();
47                    if question_count > 3 {
48                        ValidationResult::with_warning(LintWarning::PerformanceWarning {
49                            span: span.clone(),
50                            message: "Multiple replacement characters may impact performance"
51                                .to_string(),
52                        })
53                    } else {
54                        ValidationResult::new()
55                    }
56                }
57                _ => ValidationResult::new(),
58            },
59            Expression::BooleanOp {
60                operator: BooleanOperator::Or,
61                left,
62                right,
63                span,
64            } => {
65                // Warn about multiple wildcards in OR operations
66                if let (
67                    Expression::Term {
68                        term: Term::Wildcard { .. },
69                        ..
70                    },
71                    Some(right_expr),
72                ) = (left.as_ref(), right.as_ref())
73                {
74                    if let Expression::Term {
75                        term: Term::Wildcard { .. },
76                        ..
77                    } = right_expr.as_ref()
78                    {
79                        return ValidationResult::with_warning(LintWarning::PerformanceWarning {
80                            span: span.clone(),
81                            message: "Multiple wildcards in OR operations may significantly impact performance".to_string(),
82                        });
83                    }
84                }
85                ValidationResult::new()
86            }
87            _ => ValidationResult::new(),
88        }
89    }
90
91    fn can_validate(&self, expr: &Expression) -> bool {
92        match expr {
93            Expression::Term { term, .. } => {
94                matches!(term, Term::Wildcard { .. } | Term::Replacement { .. })
95            }
96            Expression::BooleanOp {
97                operator: BooleanOperator::Or,
98                ..
99            } => true,
100            _ => false,
101        }
102    }
103}
104
105// Short term performance validation rule
106pub struct ShortTermRule;
107
108impl ValidationRule for ShortTermRule {
109    fn name(&self) -> &'static str {
110        "short-term"
111    }
112
113    fn validate(&self, expr: &Expression, _ctx: &ValidationContext) -> ValidationResult {
114        match expr {
115            Expression::Term { term, span } => {
116                match term {
117                    Term::Word { value } => {
118                        let mut result = ValidationResult::new();
119
120                        // Check for empty terms
121                        if value.trim().is_empty() {
122                            result.errors.push(LintError::ValidationError {
123                                span: span.clone(),
124                                message: "Word cannot be empty".to_string(),
125                            });
126                        }
127
128                        if value.contains(':') {
129                            let parts: Vec<&str> = value.split(':').collect();
130                            if parts.len() == 2 {
131                                let field_part = parts[0];
132                                if !field_part.is_empty() && FieldType::parse(field_part).is_none()
133                                {
134                                    result.errors.push(LintError::ValidationError {
135                                        span: span.clone(),
136                                        message: format!("Unknown field type: {}", field_part),
137                                    });
138                                }
139                            }
140                        }
141
142                        result
143                    }
144                    Term::Phrase { value } => {
145                        if value.trim().is_empty() {
146                            ValidationResult::with_error(LintError::ValidationError {
147                                span: span.clone(),
148                                message: "Quoted phrase cannot be empty".to_string(),
149                            })
150                        } else {
151                            ValidationResult::new()
152                        }
153                    }
154                    Term::Hashtag { value } => {
155                        if value.trim().is_empty() {
156                            ValidationResult::with_error(LintError::ValidationError {
157                                span: span.clone(),
158                                message: "Hashtag cannot be empty".to_string(),
159                            })
160                        } else if value.starts_with('*') || value.starts_with('?') {
161                            ValidationResult::with_warning(LintWarning::PerformanceWarning {
162                                span: span.clone(),
163                                message: "Wildcard usage after '#' is discouraged and may lead to unexpected results".to_string(),
164                            })
165                        } else {
166                            ValidationResult::new()
167                        }
168                    }
169                    Term::Mention { value } => {
170                        if value.trim().is_empty() {
171                            ValidationResult::with_error(LintError::ValidationError {
172                                span: span.clone(),
173                                message: "Mention cannot be empty".to_string(),
174                            })
175                        } else if value.starts_with('*') || value.starts_with('?') {
176                            ValidationResult::with_warning(LintWarning::PerformanceWarning {
177                                span: span.clone(),
178                                message: "Wildcard usage after '@' is discouraged and may lead to unexpected results".to_string(),
179                            })
180                        } else {
181                            ValidationResult::new()
182                        }
183                    }
184                    Term::CaseSensitive { .. } => ValidationResult::new(),
185                    _ => ValidationResult::new(),
186                }
187            }
188            Expression::Comment { text, span } => {
189                if text.trim().is_empty() {
190                    ValidationResult::with_warning(LintWarning::PerformanceWarning {
191                        span: span.clone(),
192                        message: "Empty comment".to_string(),
193                    })
194                } else {
195                    ValidationResult::new()
196                }
197            }
198            _ => ValidationResult::new(),
199        }
200    }
201
202    fn can_validate(&self, expr: &Expression) -> bool {
203        matches!(expr, Expression::Term { .. } | Expression::Comment { .. })
204    }
205}
206
207// Range performance validation rule
208pub struct RangePerformanceRule;
209
210impl ValidationRule for RangePerformanceRule {
211    fn name(&self) -> &'static str {
212        "range-performance"
213    }
214
215    fn validate(&self, expr: &Expression, _ctx: &ValidationContext) -> ValidationResult {
216        if let Expression::Range {
217            field: Some(FieldType::AuthorFollowers),
218            start,
219            end,
220            span,
221        } = expr
222        {
223            if let (Ok(start_num), Ok(end_num)) = (start.parse::<i64>(), end.parse::<i64>()) {
224                let mut result = ValidationResult::new();
225
226                if start_num < 0 || end_num < 0 {
227                    result.errors.push(LintError::ValidationError {
228                        span: span.clone(),
229                        message: "Follower counts cannot be negative".to_string(),
230                    });
231                }
232
233                if end_num > 1_000_000_000 {
234                    result.warnings.push(LintWarning::PerformanceWarning {
235                        span: span.clone(),
236                        message: "Very large follower counts may not match any results".to_string(),
237                    });
238                }
239
240                result
241            } else {
242                ValidationResult::new()
243            }
244        } else {
245            ValidationResult::new()
246        }
247    }
248
249    fn can_validate(&self, expr: &Expression) -> bool {
250        matches!(
251            expr,
252            Expression::Range {
253                field: Some(FieldType::AuthorFollowers),
254                ..
255            }
256        )
257    }
258}