bwq_lint/validation/rules/
performance_rules.rs1use crate::ast::*;
2use crate::error::{LintError, LintWarning};
3use crate::validation::{ValidationContext, ValidationResult, ValidationRule};
4
5pub 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 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
105pub 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 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
207pub 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}