/home/noah/src/ruchy/src/quality/linter.rs
Line | Count | Source |
1 | | // Code linter for Ruchy with comprehensive variable tracking |
2 | | // Toyota Way: Catch issues early through static analysis |
3 | | |
4 | | use anyhow::Result; |
5 | | use crate::frontend::ast::{Expr, ExprKind, Pattern}; |
6 | | use serde::{Serialize, Deserialize}; |
7 | | use std::collections::HashMap; |
8 | | |
9 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
10 | | pub struct LintIssue { |
11 | | pub line: usize, |
12 | | pub column: usize, |
13 | | pub severity: String, |
14 | | pub rule: String, |
15 | | pub message: String, |
16 | | pub suggestion: String, |
17 | | #[serde(rename = "type")] |
18 | | pub issue_type: String, |
19 | | pub name: String, |
20 | | } |
21 | | |
22 | | #[derive(Debug, Clone)] |
23 | | pub enum LintRule { |
24 | | UnusedVariable, |
25 | | UndefinedVariable, |
26 | | VariableShadowing, |
27 | | UnusedParameter, |
28 | | UnusedLoopVariable, |
29 | | UnusedMatchBinding, |
30 | | ComplexityLimit, |
31 | | NamingConvention, |
32 | | StyleViolation, |
33 | | Security, |
34 | | Performance, |
35 | | } |
36 | | |
37 | | #[derive(Debug, Clone)] |
38 | | struct Scope { |
39 | | variables: HashMap<String, VariableInfo>, |
40 | | parent: Option<Box<Scope>>, |
41 | | } |
42 | | |
43 | | #[derive(Debug, Clone)] |
44 | | struct VariableInfo { |
45 | | defined_at: (usize, usize), |
46 | | used: bool, |
47 | | var_type: VarType, |
48 | | } |
49 | | |
50 | | #[derive(Debug, Clone)] |
51 | | enum VarType { |
52 | | Local, |
53 | | Parameter, |
54 | | LoopVariable, |
55 | | MatchBinding, |
56 | | } |
57 | | |
58 | | impl Scope { |
59 | 0 | fn new() -> Self { |
60 | 0 | Self { |
61 | 0 | variables: HashMap::new(), |
62 | 0 | parent: None, |
63 | 0 | } |
64 | 0 | } |
65 | | |
66 | 0 | fn with_parent(parent: Scope) -> Self { |
67 | 0 | Self { |
68 | 0 | variables: HashMap::new(), |
69 | 0 | parent: Some(Box::new(parent)), |
70 | 0 | } |
71 | 0 | } |
72 | | |
73 | 0 | fn define(&mut self, name: String, line: usize, column: usize, var_type: VarType) { |
74 | 0 | self.variables.insert(name, VariableInfo { |
75 | 0 | defined_at: (line, column), |
76 | 0 | used: false, |
77 | 0 | var_type, |
78 | 0 | }); |
79 | 0 | } |
80 | | |
81 | 0 | fn mark_used(&mut self, name: &str) -> bool { |
82 | 0 | if let Some(info) = self.variables.get_mut(name) { |
83 | 0 | info.used = true; |
84 | 0 | true |
85 | 0 | } else if let Some(parent) = &mut self.parent { |
86 | 0 | parent.mark_used(name) |
87 | | } else { |
88 | 0 | false |
89 | | } |
90 | 0 | } |
91 | | |
92 | 0 | fn is_defined(&self, name: &str) -> bool { |
93 | 0 | self.variables.contains_key(name) || |
94 | 0 | self.parent.as_ref().is_some_and(|p| p.is_defined(name)) |
95 | 0 | } |
96 | | |
97 | 0 | fn is_shadowing(&self, name: &str) -> bool { |
98 | 0 | self.parent.as_ref().is_some_and(|p| p.is_defined(name)) |
99 | 0 | } |
100 | | } |
101 | | |
102 | | pub struct Linter { |
103 | | rules: Vec<LintRule>, |
104 | | strict_mode: bool, |
105 | | max_complexity: usize, |
106 | | } |
107 | | |
108 | | impl Linter { |
109 | 0 | pub fn new() -> Self { |
110 | 0 | Self { |
111 | 0 | rules: vec![ |
112 | 0 | LintRule::UnusedVariable, |
113 | 0 | LintRule::UndefinedVariable, |
114 | 0 | LintRule::VariableShadowing, |
115 | 0 | LintRule::UnusedParameter, |
116 | 0 | LintRule::UnusedLoopVariable, |
117 | 0 | LintRule::UnusedMatchBinding, |
118 | 0 | LintRule::ComplexityLimit, |
119 | 0 | LintRule::NamingConvention, |
120 | 0 | ], |
121 | 0 | strict_mode: false, |
122 | 0 | max_complexity: 10, |
123 | 0 | } |
124 | 0 | } |
125 | | |
126 | 0 | pub fn set_rules(&mut self, rule_filter: &str) { |
127 | 0 | self.rules.clear(); |
128 | 0 | for rule in rule_filter.split(',') { |
129 | 0 | match rule.trim() { |
130 | 0 | "unused" => { |
131 | 0 | self.rules.push(LintRule::UnusedVariable); |
132 | 0 | self.rules.push(LintRule::UnusedParameter); |
133 | 0 | self.rules.push(LintRule::UnusedLoopVariable); |
134 | 0 | self.rules.push(LintRule::UnusedMatchBinding); |
135 | 0 | } |
136 | 0 | "undefined" => self.rules.push(LintRule::UndefinedVariable), |
137 | 0 | "shadowing" => self.rules.push(LintRule::VariableShadowing), |
138 | 0 | "complexity" => self.rules.push(LintRule::ComplexityLimit), |
139 | 0 | "style" => self.rules.push(LintRule::StyleViolation), |
140 | 0 | "security" => self.rules.push(LintRule::Security), |
141 | 0 | "performance" => self.rules.push(LintRule::Performance), |
142 | 0 | _ => {} |
143 | | } |
144 | | } |
145 | 0 | } |
146 | | |
147 | 0 | pub fn set_strict_mode(&mut self, strict: bool) { |
148 | 0 | self.strict_mode = strict; |
149 | 0 | } |
150 | | |
151 | 0 | pub fn lint(&self, ast: &Expr, _source: &str) -> Result<Vec<LintIssue>> { |
152 | 0 | let mut issues = Vec::new(); |
153 | 0 | let mut scope = Scope::new(); |
154 | | |
155 | | // Analyze the AST with variable tracking |
156 | 0 | self.analyze_expr(ast, &mut scope, &mut issues); |
157 | | |
158 | | // Check for unused variables |
159 | 0 | self.check_unused_in_scope(&scope, &mut issues); |
160 | | |
161 | | // Check complexity |
162 | 0 | if self.rules.iter().any(|r| matches!(r, LintRule::ComplexityLimit)) |
163 | 0 | && self.calculate_complexity(ast) > self.max_complexity { |
164 | 0 | issues.push(LintIssue { |
165 | | line: 1, |
166 | | column: 1, |
167 | 0 | severity: if self.strict_mode { "error" } else { "warning" }.to_string(), |
168 | 0 | rule: "complexity".to_string(), |
169 | 0 | message: format!("Function complexity exceeds limit of {}", self.max_complexity), |
170 | 0 | suggestion: "Consider breaking this into smaller functions".to_string(), |
171 | 0 | issue_type: "complexity".to_string(), |
172 | 0 | name: String::new(), |
173 | | }); |
174 | 0 | } |
175 | | |
176 | | // Return empty if clean |
177 | 0 | if issues.is_empty() { |
178 | | // For JSON format compatibility |
179 | 0 | return Ok(vec![]); |
180 | 0 | } |
181 | | |
182 | 0 | Ok(issues) |
183 | 0 | } |
184 | | |
185 | 0 | fn analyze_expr(&self, expr: &Expr, scope: &mut Scope, issues: &mut Vec<LintIssue>) { |
186 | 0 | match &expr.kind { |
187 | 0 | ExprKind::Let { name, value, body, .. } => { |
188 | | // Analyze the value first (with current scope) |
189 | 0 | self.analyze_expr(value, scope, issues); |
190 | | |
191 | | // Create new scope for the let binding body |
192 | 0 | let mut let_scope = Scope::with_parent(scope.clone()); |
193 | | |
194 | | // Check for shadowing before defining |
195 | 0 | if self.rules.iter().any(|r| matches!(r, LintRule::VariableShadowing)) |
196 | 0 | && let_scope.is_shadowing(name) { |
197 | 0 | issues.push(LintIssue { |
198 | 0 | line: 3, // Simplified line tracking |
199 | 0 | column: 1, |
200 | 0 | severity: "warning".to_string(), |
201 | 0 | rule: "shadowing".to_string(), |
202 | 0 | message: format!("variable shadowing: {name}"), |
203 | 0 | suggestion: format!("Consider renaming variable '{name}'"), |
204 | 0 | issue_type: "variable_shadowing".to_string(), |
205 | 0 | name: name.clone(), |
206 | 0 | }); |
207 | 0 | } |
208 | | |
209 | | // Define the variable in the new scope |
210 | 0 | let_scope.define(name.clone(), 2, 1, VarType::Local); |
211 | | |
212 | | // Analyze the body with the new scope |
213 | 0 | self.analyze_expr(body, &mut let_scope, issues); |
214 | | |
215 | | // Check for unused variables in the let scope |
216 | 0 | self.check_unused_in_scope(&let_scope, issues); |
217 | | } |
218 | | |
219 | 0 | ExprKind::Identifier(name) => { |
220 | | // Special case: println is a built-in, not an undefined variable |
221 | 0 | if name == "println" || name == "print" || name == "eprintln" { |
222 | 0 | return; |
223 | 0 | } |
224 | | |
225 | | // Mark as used if defined, otherwise report as undefined |
226 | 0 | if !scope.mark_used(name) |
227 | 0 | && self.rules.iter().any(|r| matches!(r, LintRule::UndefinedVariable)) { |
228 | 0 | issues.push(LintIssue { |
229 | 0 | line: 3, |
230 | 0 | column: 1, |
231 | 0 | severity: "error".to_string(), |
232 | 0 | rule: "undefined".to_string(), |
233 | 0 | message: format!("undefined variable: {name}"), |
234 | 0 | suggestion: format!("Define '{name}' before using it"), |
235 | 0 | issue_type: "undefined_variable".to_string(), |
236 | 0 | name: name.clone(), |
237 | 0 | }); |
238 | 0 | } |
239 | | } |
240 | | |
241 | 0 | ExprKind::Function { name, params, body, .. } => { |
242 | | // Define the function name in the current scope |
243 | 0 | scope.define(name.clone(), 1, 1, VarType::Local); |
244 | | |
245 | | // Create new scope for function body |
246 | 0 | let mut func_scope = Scope::with_parent(scope.clone()); |
247 | | |
248 | | // Add parameters to scope with correct type |
249 | 0 | for param in params { |
250 | 0 | self.extract_param_bindings(¶m.pattern, &mut func_scope); |
251 | 0 | } |
252 | | |
253 | | // Analyze function body |
254 | 0 | self.analyze_expr(body, &mut func_scope, issues); |
255 | | |
256 | | // Check for unused variables in function body (but not parameters for now) |
257 | | // Parameters might be part of public API |
258 | 0 | for (name, info) in &func_scope.variables { |
259 | 0 | if !info.used && matches!(info.var_type, VarType::Local) { |
260 | 0 | issues.push(LintIssue { |
261 | 0 | line: info.defined_at.0, |
262 | 0 | column: info.defined_at.1, |
263 | 0 | severity: "warning".to_string(), |
264 | 0 | rule: "unused_variable".to_string(), |
265 | 0 | message: format!("unused variable: {name}"), |
266 | 0 | suggestion: format!("Remove unused variable '{name}'"), |
267 | 0 | issue_type: "unused_variable".to_string(), |
268 | 0 | name: name.clone(), |
269 | 0 | }); |
270 | 0 | } |
271 | | } |
272 | | } |
273 | | |
274 | 0 | ExprKind::For { var, pattern, iter, body, .. } => { |
275 | | // Create new scope for loop |
276 | 0 | let mut loop_scope = Scope::with_parent(scope.clone()); |
277 | | |
278 | | // Add loop variable to scope |
279 | 0 | if let Some(pat) = pattern { |
280 | 0 | self.extract_loop_bindings(pat, &mut loop_scope); |
281 | 0 | } else { |
282 | 0 | // Fall back to var field for backward compatibility |
283 | 0 | loop_scope.define(var.clone(), 2, 1, VarType::LoopVariable); |
284 | 0 | } |
285 | | |
286 | | // Analyze iterator |
287 | 0 | self.analyze_expr(iter, scope, issues); |
288 | | |
289 | | // Analyze loop body |
290 | 0 | self.analyze_expr(body, &mut loop_scope, issues); |
291 | | |
292 | | // Check for unused loop variables |
293 | 0 | self.check_unused_in_scope(&loop_scope, issues); |
294 | | } |
295 | | |
296 | 0 | ExprKind::Match { expr, arms, .. } => { |
297 | | // Analyze scrutinee |
298 | 0 | self.analyze_expr(expr, scope, issues); |
299 | | |
300 | | // Analyze each branch |
301 | 0 | for arm in arms { |
302 | 0 | let mut branch_scope = Scope::with_parent(scope.clone()); |
303 | | |
304 | | // Add pattern bindings to scope |
305 | 0 | self.extract_pattern_bindings(&arm.pattern, &mut branch_scope); |
306 | | |
307 | | // Analyze guard if present |
308 | 0 | if let Some(guard) = &arm.guard { |
309 | 0 | self.analyze_expr(guard, &mut branch_scope, issues); |
310 | 0 | } |
311 | | |
312 | | // Analyze branch expression |
313 | 0 | self.analyze_expr(&arm.body, &mut branch_scope, issues); |
314 | | |
315 | | // Check for unused match bindings |
316 | 0 | self.check_unused_in_scope(&branch_scope, issues); |
317 | | } |
318 | | } |
319 | | |
320 | 0 | ExprKind::If { condition, then_branch, else_branch, .. } => { |
321 | 0 | self.analyze_expr(condition, scope, issues); |
322 | | |
323 | | // Create new scope for then branch |
324 | 0 | let mut then_scope = Scope::with_parent(scope.clone()); |
325 | 0 | self.analyze_expr(then_branch, &mut then_scope, issues); |
326 | | |
327 | | // Create new scope for else branch if exists |
328 | 0 | if let Some(else_expr) = else_branch { |
329 | 0 | let mut else_scope = Scope::with_parent(scope.clone()); |
330 | 0 | self.analyze_expr(else_expr, &mut else_scope, issues); |
331 | 0 | } |
332 | | } |
333 | | |
334 | 0 | ExprKind::Block(exprs) => { |
335 | | // For blocks, we use the same scope level - each statement can see previous ones |
336 | 0 | for expr in exprs { |
337 | 0 | self.analyze_expr(expr, scope, issues); |
338 | 0 | } |
339 | | } |
340 | | |
341 | 0 | ExprKind::Binary { left, right, .. } => { |
342 | 0 | self.analyze_expr(left, scope, issues); |
343 | 0 | self.analyze_expr(right, scope, issues); |
344 | 0 | } |
345 | | |
346 | 0 | ExprKind::Call { func, args, .. } => { |
347 | 0 | self.analyze_expr(func, scope, issues); |
348 | 0 | for arg in args { |
349 | 0 | self.analyze_expr(arg, scope, issues); |
350 | 0 | } |
351 | | } |
352 | | |
353 | 0 | ExprKind::MethodCall { receiver, args, .. } => { |
354 | 0 | self.analyze_expr(receiver, scope, issues); |
355 | 0 | for arg in args { |
356 | 0 | self.analyze_expr(arg, scope, issues); |
357 | 0 | } |
358 | | } |
359 | | |
360 | 0 | ExprKind::StringInterpolation { parts } => { |
361 | | // Analyze expressions within f-string interpolations |
362 | 0 | for part in parts { |
363 | 0 | match part { |
364 | 0 | crate::frontend::ast::StringPart::Expr(expr) => { |
365 | 0 | self.analyze_expr(expr, scope, issues); |
366 | 0 | } |
367 | 0 | crate::frontend::ast::StringPart::ExprWithFormat { expr, .. } => { |
368 | 0 | self.analyze_expr(expr, scope, issues); |
369 | 0 | } |
370 | 0 | crate::frontend::ast::StringPart::Text(_) => { |
371 | 0 | // Literal text, nothing to analyze |
372 | 0 | } |
373 | | } |
374 | | } |
375 | | } |
376 | | |
377 | 0 | ExprKind::Lambda { params, body, .. } => { |
378 | | // Create new scope for lambda body |
379 | 0 | let mut lambda_scope = Scope::with_parent(scope.clone()); |
380 | | |
381 | | // Add parameters to scope |
382 | 0 | for param in params { |
383 | 0 | self.extract_param_bindings(¶m.pattern, &mut lambda_scope); |
384 | 0 | } |
385 | | |
386 | | // Analyze lambda body |
387 | 0 | self.analyze_expr(body, &mut lambda_scope, issues); |
388 | | |
389 | | // Check for unused parameters |
390 | 0 | self.check_unused_in_scope(&lambda_scope, issues); |
391 | | } |
392 | | |
393 | 0 | ExprKind::Return { value } => { |
394 | 0 | if let Some(expr) = value { |
395 | 0 | self.analyze_expr(expr, scope, issues); |
396 | 0 | } |
397 | | } |
398 | | |
399 | 0 | ExprKind::List(exprs) | ExprKind::Tuple(exprs) => { |
400 | 0 | for expr in exprs { |
401 | 0 | self.analyze_expr(expr, scope, issues); |
402 | 0 | } |
403 | | } |
404 | | |
405 | 0 | ExprKind::FieldAccess { object, .. } => { |
406 | 0 | self.analyze_expr(object, scope, issues); |
407 | 0 | } |
408 | | |
409 | 0 | ExprKind::IndexAccess { object, index } => { |
410 | 0 | self.analyze_expr(object, scope, issues); |
411 | 0 | self.analyze_expr(index, scope, issues); |
412 | 0 | } |
413 | | |
414 | 0 | ExprKind::While { condition, body, .. } => { |
415 | 0 | self.analyze_expr(condition, scope, issues); |
416 | 0 | self.analyze_expr(body, scope, issues); |
417 | 0 | } |
418 | | |
419 | 0 | ExprKind::Assign { target, value, .. } => { |
420 | 0 | self.analyze_expr(target, scope, issues); |
421 | 0 | self.analyze_expr(value, scope, issues); |
422 | 0 | } |
423 | | |
424 | 0 | _ => { |
425 | 0 | // Handle other expression types as needed |
426 | 0 | } |
427 | | } |
428 | 0 | } |
429 | | |
430 | 0 | fn extract_loop_bindings(&self, pattern: &Pattern, scope: &mut Scope) { |
431 | 0 | match pattern { |
432 | 0 | Pattern::Identifier(name) => { |
433 | | // Check if it's a special identifier like _ |
434 | 0 | if name != "_" { |
435 | 0 | scope.define(name.clone(), 2, 1, VarType::LoopVariable); |
436 | 0 | } |
437 | | } |
438 | 0 | Pattern::Tuple(patterns) => { |
439 | 0 | for p in patterns { |
440 | 0 | self.extract_loop_bindings(p, scope); |
441 | 0 | } |
442 | | } |
443 | 0 | Pattern::Struct { fields, .. } => { |
444 | 0 | for field in fields { |
445 | 0 | if let Some(pattern) = &field.pattern { |
446 | 0 | self.extract_loop_bindings(pattern, scope); |
447 | 0 | } else { |
448 | 0 | // Shorthand: { x } means { x: x }, bind the name |
449 | 0 | scope.define(field.name.clone(), 2, 1, VarType::LoopVariable); |
450 | 0 | } |
451 | | } |
452 | | } |
453 | 0 | Pattern::List(patterns) => { |
454 | 0 | for p in patterns { |
455 | 0 | self.extract_loop_bindings(p, scope); |
456 | 0 | } |
457 | | } |
458 | 0 | _ => {} |
459 | | } |
460 | 0 | } |
461 | | |
462 | 0 | fn extract_param_bindings(&self, pattern: &Pattern, scope: &mut Scope) { |
463 | 0 | match pattern { |
464 | 0 | Pattern::Identifier(name) => { |
465 | | // Check if it's a special identifier like _ |
466 | 0 | if name != "_" { |
467 | 0 | scope.define(name.clone(), 1, 1, VarType::Parameter); |
468 | 0 | } |
469 | | } |
470 | 0 | Pattern::Tuple(patterns) => { |
471 | 0 | for p in patterns { |
472 | 0 | self.extract_param_bindings(p, scope); |
473 | 0 | } |
474 | | } |
475 | 0 | Pattern::Struct { fields, .. } => { |
476 | 0 | for field in fields { |
477 | 0 | if let Some(pattern) = &field.pattern { |
478 | 0 | self.extract_param_bindings(pattern, scope); |
479 | 0 | } else { |
480 | 0 | // Shorthand: { x } means { x: x }, bind the name |
481 | 0 | scope.define(field.name.clone(), 1, 1, VarType::Parameter); |
482 | 0 | } |
483 | | } |
484 | | } |
485 | 0 | Pattern::List(patterns) => { |
486 | 0 | for p in patterns { |
487 | 0 | self.extract_param_bindings(p, scope); |
488 | 0 | } |
489 | | } |
490 | 0 | _ => {} |
491 | | } |
492 | 0 | } |
493 | | |
494 | 0 | fn extract_pattern_bindings(&self, pattern: &Pattern, scope: &mut Scope) { |
495 | 0 | match pattern { |
496 | 0 | Pattern::Identifier(name) => { |
497 | | // Check if it's a special identifier like _ |
498 | 0 | if name != "_" { |
499 | 0 | scope.define(name.clone(), 3, 1, VarType::MatchBinding); |
500 | 0 | } |
501 | | } |
502 | 0 | Pattern::Tuple(patterns) => { |
503 | 0 | for p in patterns { |
504 | 0 | self.extract_pattern_bindings(p, scope); |
505 | 0 | } |
506 | | } |
507 | 0 | Pattern::Struct { fields, .. } => { |
508 | 0 | for field in fields { |
509 | 0 | if let Some(pattern) = &field.pattern { |
510 | 0 | self.extract_pattern_bindings(pattern, scope); |
511 | 0 | } else { |
512 | 0 | // Shorthand: { x } means { x: x }, bind the name |
513 | 0 | scope.define(field.name.clone(), 3, 1, VarType::MatchBinding); |
514 | 0 | } |
515 | | } |
516 | | } |
517 | 0 | Pattern::List(patterns) => { |
518 | 0 | for p in patterns { |
519 | 0 | self.extract_pattern_bindings(p, scope); |
520 | 0 | } |
521 | | } |
522 | 0 | Pattern::Some(inner) | Pattern::Ok(inner) | Pattern::Err(inner) => { |
523 | 0 | self.extract_pattern_bindings(inner, scope); |
524 | 0 | } |
525 | 0 | _ => {} |
526 | | } |
527 | 0 | } |
528 | | |
529 | 0 | fn check_unused_in_scope(&self, scope: &Scope, issues: &mut Vec<LintIssue>) { |
530 | 0 | for (name, info) in &scope.variables { |
531 | 0 | if !info.used { |
532 | 0 | let (rule_type, message) = match info.var_type { |
533 | | VarType::Local => { |
534 | 0 | if self.rules.iter().any(|r| matches!(r, LintRule::UnusedVariable)) { |
535 | 0 | ("unused_variable", format!("unused variable: {name}")) |
536 | | } else { |
537 | 0 | continue; |
538 | | } |
539 | | } |
540 | | VarType::Parameter => { |
541 | 0 | if self.rules.iter().any(|r| matches!(r, LintRule::UnusedParameter)) { |
542 | 0 | ("unused_parameter", format!("unused parameter: {name}")) |
543 | | } else { |
544 | 0 | continue; |
545 | | } |
546 | | } |
547 | | VarType::LoopVariable => { |
548 | 0 | if self.rules.iter().any(|r| matches!(r, LintRule::UnusedLoopVariable)) { |
549 | 0 | ("unused_loop_variable", format!("unused loop variable: {name}")) |
550 | | } else { |
551 | 0 | continue; |
552 | | } |
553 | | } |
554 | | VarType::MatchBinding => { |
555 | 0 | if self.rules.iter().any(|r| matches!(r, LintRule::UnusedMatchBinding)) { |
556 | 0 | ("unused_match_binding", format!("unused match binding: {name}")) |
557 | | } else { |
558 | 0 | continue; |
559 | | } |
560 | | } |
561 | | }; |
562 | | |
563 | 0 | issues.push(LintIssue { |
564 | 0 | line: info.defined_at.0, |
565 | 0 | column: info.defined_at.1, |
566 | 0 | severity: "warning".to_string(), |
567 | 0 | rule: rule_type.to_string(), |
568 | 0 | message: message.clone(), |
569 | 0 | suggestion: format!("Remove unused {}", |
570 | 0 | match info.var_type { |
571 | 0 | VarType::Local => "variable", |
572 | 0 | VarType::Parameter => "parameter", |
573 | 0 | VarType::LoopVariable => "loop variable", |
574 | 0 | VarType::MatchBinding => "match binding", |
575 | | } |
576 | | ), |
577 | 0 | issue_type: rule_type.to_string(), |
578 | 0 | name: name.clone(), |
579 | | }); |
580 | 0 | } |
581 | | } |
582 | 0 | } |
583 | | |
584 | 0 | pub fn auto_fix(&self, source: &str, issues: &[LintIssue]) -> Result<String> { |
585 | | // Simple auto-fix implementation |
586 | 0 | let mut fixed = source.to_string(); |
587 | | |
588 | 0 | for issue in issues { |
589 | 0 | if issue.rule == "style" { |
590 | 0 | // Fix style issues |
591 | 0 | fixed = fixed.replace(" ", " "); |
592 | 0 | } |
593 | | } |
594 | | |
595 | 0 | Ok(fixed) |
596 | 0 | } |
597 | | |
598 | 0 | fn calculate_complexity(&self, expr: &Expr) -> usize { |
599 | 0 | match &expr.kind { |
600 | 0 | ExprKind::If { condition: _, then_branch, else_branch, .. } => { |
601 | 0 | 1 + self.calculate_complexity(then_branch) |
602 | 0 | + else_branch.as_ref().map_or(0, |e| self.calculate_complexity(e)) |
603 | | } |
604 | 0 | ExprKind::Match { .. } => 2, |
605 | 0 | ExprKind::While { .. } | ExprKind::For { .. } => 2, |
606 | 0 | ExprKind::Block(exprs) => { |
607 | 0 | exprs.iter().map(|e| self.calculate_complexity(e)).sum() |
608 | | } |
609 | 0 | _ => 0, |
610 | | } |
611 | 0 | } |
612 | | } |
613 | | |
614 | | impl Default for Linter { |
615 | 0 | fn default() -> Self { |
616 | 0 | Self::new() |
617 | 0 | } |
618 | | } |
619 | | |
620 | | #[cfg(test)] |
621 | | mod tests { |
622 | | use super::*; |
623 | | use crate::frontend::ast::{ |
624 | | Expr, ExprKind, Pattern, Literal, BinaryOp, Span, Param, Type, TypeKind, |
625 | | MatchArm, StringPart, StructPatternField |
626 | | }; |
627 | | |
628 | | // Helper functions for consistent test setup |
629 | | fn create_test_span() -> Span { |
630 | | Span { start: 0, end: 1 } |
631 | | } |
632 | | |
633 | | fn create_test_linter() -> Linter { |
634 | | Linter::new() |
635 | | } |
636 | | |
637 | | fn create_test_linter_with_rules(rules: &str) -> Linter { |
638 | | let mut linter = Linter::new(); |
639 | | linter.set_rules(rules); |
640 | | linter |
641 | | } |
642 | | |
643 | | fn create_test_expr_literal_int(value: i64) -> Expr { |
644 | | Expr::new(ExprKind::Literal(Literal::Integer(value)), create_test_span()) |
645 | | } |
646 | | |
647 | | fn create_test_expr_identifier(name: &str) -> Expr { |
648 | | Expr::new(ExprKind::Identifier(name.to_string()), create_test_span()) |
649 | | } |
650 | | |
651 | | fn create_test_expr_let(name: &str, value: Expr, body: Expr) -> Expr { |
652 | | Expr::new(ExprKind::Let { |
653 | | name: name.to_string(), |
654 | | type_annotation: None, |
655 | | value: Box::new(value), |
656 | | body: Box::new(body), |
657 | | is_mutable: false, |
658 | | }, create_test_span()) |
659 | | } |
660 | | |
661 | | fn create_test_expr_function(name: &str, params: Vec<Param>, body: Expr) -> Expr { |
662 | | Expr::new(ExprKind::Function { |
663 | | name: name.to_string(), |
664 | | type_params: vec![], |
665 | | params, |
666 | | return_type: None, |
667 | | body: Box::new(body), |
668 | | is_async: false, |
669 | | is_pub: false, |
670 | | }, create_test_span()) |
671 | | } |
672 | | |
673 | | fn create_test_param(name: &str) -> Param { |
674 | | Param { |
675 | | pattern: Pattern::Identifier(name.to_string()), |
676 | | ty: Type { |
677 | | kind: TypeKind::Named("Any".to_string()), |
678 | | span: create_test_span(), |
679 | | }, |
680 | | span: create_test_span(), |
681 | | is_mutable: false, |
682 | | default_value: None, |
683 | | } |
684 | | } |
685 | | |
686 | | fn create_test_expr_block(exprs: Vec<Expr>) -> Expr { |
687 | | Expr::new(ExprKind::Block(exprs), create_test_span()) |
688 | | } |
689 | | |
690 | | fn create_test_expr_binary(op: BinaryOp, left: Expr, right: Expr) -> Expr { |
691 | | Expr::new(ExprKind::Binary { |
692 | | op, |
693 | | left: Box::new(left), |
694 | | right: Box::new(right), |
695 | | }, create_test_span()) |
696 | | } |
697 | | |
698 | | fn create_test_expr_call(func: Expr, args: Vec<Expr>) -> Expr { |
699 | | Expr::new(ExprKind::Call { |
700 | | func: Box::new(func), |
701 | | args, |
702 | | }, create_test_span()) |
703 | | } |
704 | | |
705 | | fn create_test_expr_if(condition: Expr, then_branch: Expr, else_branch: Option<Expr>) -> Expr { |
706 | | Expr::new(ExprKind::If { |
707 | | condition: Box::new(condition), |
708 | | then_branch: Box::new(then_branch), |
709 | | else_branch: else_branch.map(Box::new), |
710 | | }, create_test_span()) |
711 | | } |
712 | | |
713 | | fn create_test_expr_for(var: &str, pattern: Option<Pattern>, iter: Expr, body: Expr) -> Expr { |
714 | | Expr::new(ExprKind::For { |
715 | | var: var.to_string(), |
716 | | pattern, |
717 | | iter: Box::new(iter), |
718 | | body: Box::new(body), |
719 | | }, create_test_span()) |
720 | | } |
721 | | |
722 | | fn create_test_expr_match(expr: Expr, arms: Vec<MatchArm>) -> Expr { |
723 | | Expr::new(ExprKind::Match { |
724 | | expr: Box::new(expr), |
725 | | arms, |
726 | | }, create_test_span()) |
727 | | } |
728 | | |
729 | | fn create_test_match_arm(pattern: Pattern, body: Expr) -> MatchArm { |
730 | | MatchArm { |
731 | | pattern, |
732 | | guard: None, |
733 | | body: Box::new(body), |
734 | | span: create_test_span(), |
735 | | } |
736 | | } |
737 | | |
738 | | fn create_test_expr_lambda(params: Vec<Param>, body: Expr) -> Expr { |
739 | | Expr::new(ExprKind::Lambda { |
740 | | params, |
741 | | body: Box::new(body), |
742 | | }, create_test_span()) |
743 | | } |
744 | | |
745 | | fn create_test_expr_method_call(receiver: Expr, method: &str, args: Vec<Expr>) -> Expr { |
746 | | Expr::new(ExprKind::MethodCall { |
747 | | receiver: Box::new(receiver), |
748 | | method: method.to_string(), |
749 | | args, |
750 | | }, create_test_span()) |
751 | | } |
752 | | |
753 | | fn create_test_expr_while(condition: Expr, body: Expr) -> Expr { |
754 | | Expr::new(ExprKind::While { |
755 | | condition: Box::new(condition), |
756 | | body: Box::new(body), |
757 | | }, create_test_span()) |
758 | | } |
759 | | |
760 | | fn create_test_expr_return(value: Option<Expr>) -> Expr { |
761 | | Expr::new(ExprKind::Return { |
762 | | value: value.map(Box::new), |
763 | | }, create_test_span()) |
764 | | } |
765 | | |
766 | | // ========== Linter Construction Tests ========== |
767 | | |
768 | | #[test] |
769 | | fn test_linter_creation() { |
770 | | let linter = Linter::new(); |
771 | | assert_eq!(linter.rules.len(), 8); // Default rules count |
772 | | assert!(!linter.strict_mode); |
773 | | assert_eq!(linter.max_complexity, 10); |
774 | | } |
775 | | |
776 | | #[test] |
777 | | fn test_linter_default() { |
778 | | let linter = Linter::default(); |
779 | | assert_eq!(linter.rules.len(), 8); |
780 | | assert!(!linter.strict_mode); |
781 | | assert_eq!(linter.max_complexity, 10); |
782 | | } |
783 | | |
784 | | #[test] |
785 | | fn test_linter_set_strict_mode() { |
786 | | let mut linter = Linter::new(); |
787 | | linter.set_strict_mode(true); |
788 | | assert!(linter.strict_mode); |
789 | | } |
790 | | |
791 | | // ========== Rule Configuration Tests ========== |
792 | | |
793 | | #[test] |
794 | | fn test_set_rules_unused() { |
795 | | let mut linter = Linter::new(); |
796 | | linter.set_rules("unused"); |
797 | | assert_eq!(linter.rules.len(), 4); // UnusedVariable, Parameter, LoopVariable, MatchBinding |
798 | | } |
799 | | |
800 | | #[test] |
801 | | fn test_set_rules_undefined() { |
802 | | let mut linter = Linter::new(); |
803 | | linter.set_rules("undefined"); |
804 | | assert_eq!(linter.rules.len(), 1); |
805 | | assert!(matches!(linter.rules[0], LintRule::UndefinedVariable)); |
806 | | } |
807 | | |
808 | | #[test] |
809 | | fn test_set_rules_shadowing() { |
810 | | let mut linter = Linter::new(); |
811 | | linter.set_rules("shadowing"); |
812 | | assert_eq!(linter.rules.len(), 1); |
813 | | assert!(matches!(linter.rules[0], LintRule::VariableShadowing)); |
814 | | } |
815 | | |
816 | | #[test] |
817 | | fn test_set_rules_complexity() { |
818 | | let mut linter = Linter::new(); |
819 | | linter.set_rules("complexity"); |
820 | | assert_eq!(linter.rules.len(), 1); |
821 | | assert!(matches!(linter.rules[0], LintRule::ComplexityLimit)); |
822 | | } |
823 | | |
824 | | #[test] |
825 | | fn test_set_rules_multiple() { |
826 | | let mut linter = Linter::new(); |
827 | | linter.set_rules("undefined,shadowing,complexity"); |
828 | | assert_eq!(linter.rules.len(), 3); |
829 | | } |
830 | | |
831 | | #[test] |
832 | | fn test_set_rules_unknown() { |
833 | | let mut linter = Linter::new(); |
834 | | linter.set_rules("unknown_rule"); |
835 | | assert_eq!(linter.rules.len(), 0); |
836 | | } |
837 | | |
838 | | #[test] |
839 | | fn test_set_rules_style_security_performance() { |
840 | | let mut linter = Linter::new(); |
841 | | linter.set_rules("style,security,performance"); |
842 | | assert_eq!(linter.rules.len(), 3); |
843 | | assert!(linter.rules.iter().any(|r| matches!(r, LintRule::StyleViolation))); |
844 | | assert!(linter.rules.iter().any(|r| matches!(r, LintRule::Security))); |
845 | | assert!(linter.rules.iter().any(|r| matches!(r, LintRule::Performance))); |
846 | | } |
847 | | |
848 | | // ========== Scope Tests ========== |
849 | | |
850 | | #[test] |
851 | | fn test_scope_creation() { |
852 | | let scope = Scope::new(); |
853 | | assert!(scope.variables.is_empty()); |
854 | | assert!(scope.parent.is_none()); |
855 | | } |
856 | | |
857 | | #[test] |
858 | | fn test_scope_with_parent() { |
859 | | let parent_scope = Scope::new(); |
860 | | let child_scope = Scope::with_parent(parent_scope); |
861 | | assert!(child_scope.parent.is_some()); |
862 | | } |
863 | | |
864 | | #[test] |
865 | | fn test_scope_define_variable() { |
866 | | let mut scope = Scope::new(); |
867 | | scope.define("x".to_string(), 1, 1, VarType::Local); |
868 | | assert!(scope.variables.contains_key("x")); |
869 | | assert!(!scope.variables["x"].used); |
870 | | } |
871 | | |
872 | | #[test] |
873 | | fn test_scope_mark_used() { |
874 | | let mut scope = Scope::new(); |
875 | | scope.define("x".to_string(), 1, 1, VarType::Local); |
876 | | assert!(scope.mark_used("x")); |
877 | | assert!(scope.variables["x"].used); |
878 | | } |
879 | | |
880 | | #[test] |
881 | | fn test_scope_mark_used_undefined() { |
882 | | let mut scope = Scope::new(); |
883 | | assert!(!scope.mark_used("undefined_var")); |
884 | | } |
885 | | |
886 | | #[test] |
887 | | fn test_scope_mark_used_in_parent() { |
888 | | let mut parent_scope = Scope::new(); |
889 | | parent_scope.define("x".to_string(), 1, 1, VarType::Local); |
890 | | |
891 | | let mut child_scope = Scope::with_parent(parent_scope); |
892 | | assert!(child_scope.mark_used("x")); |
893 | | } |
894 | | |
895 | | #[test] |
896 | | fn test_scope_is_defined() { |
897 | | let mut scope = Scope::new(); |
898 | | scope.define("x".to_string(), 1, 1, VarType::Local); |
899 | | assert!(scope.is_defined("x")); |
900 | | assert!(!scope.is_defined("y")); |
901 | | } |
902 | | |
903 | | #[test] |
904 | | fn test_scope_is_defined_in_parent() { |
905 | | let mut parent_scope = Scope::new(); |
906 | | parent_scope.define("x".to_string(), 1, 1, VarType::Local); |
907 | | |
908 | | let child_scope = Scope::with_parent(parent_scope); |
909 | | assert!(child_scope.is_defined("x")); |
910 | | } |
911 | | |
912 | | #[test] |
913 | | fn test_scope_is_shadowing() { |
914 | | let mut parent_scope = Scope::new(); |
915 | | parent_scope.define("x".to_string(), 1, 1, VarType::Local); |
916 | | |
917 | | let child_scope = Scope::with_parent(parent_scope); |
918 | | assert!(child_scope.is_shadowing("x")); |
919 | | assert!(!child_scope.is_shadowing("y")); |
920 | | } |
921 | | |
922 | | // ========== Lint Issue Tests ========== |
923 | | |
924 | | #[test] |
925 | | fn test_lint_issue_serialization() { |
926 | | let issue = LintIssue { |
927 | | line: 5, |
928 | | column: 10, |
929 | | severity: "warning".to_string(), |
930 | | rule: "unused_variable".to_string(), |
931 | | message: "unused variable: x".to_string(), |
932 | | suggestion: "Remove unused variable 'x'".to_string(), |
933 | | issue_type: "unused_variable".to_string(), |
934 | | name: "x".to_string(), |
935 | | }; |
936 | | |
937 | | let json = serde_json::to_string(&issue); |
938 | | assert!(json.is_ok()); |
939 | | |
940 | | let deserialized: Result<LintIssue, _> = serde_json::from_str(&json.unwrap()); |
941 | | assert!(deserialized.is_ok()); |
942 | | } |
943 | | |
944 | | // ========== Basic Linting Tests ========== |
945 | | |
946 | | #[test] |
947 | | fn test_lint_empty_expression() { |
948 | | let linter = create_test_linter(); |
949 | | let expr = create_test_expr_literal_int(42); |
950 | | |
951 | | let issues = linter.lint(&expr, "42").unwrap(); |
952 | | assert_eq!(issues.len(), 0); |
953 | | } |
954 | | |
955 | | #[test] |
956 | | fn test_lint_undefined_variable() { |
957 | | let linter = create_test_linter_with_rules("undefined"); |
958 | | let expr = create_test_expr_identifier("undefined_var"); |
959 | | |
960 | | let issues = linter.lint(&expr, "undefined_var").unwrap(); |
961 | | assert_eq!(issues.len(), 1); |
962 | | assert_eq!(issues[0].rule, "undefined"); |
963 | | assert_eq!(issues[0].name, "undefined_var"); |
964 | | assert_eq!(issues[0].severity, "error"); |
965 | | } |
966 | | |
967 | | #[test] |
968 | | fn test_lint_builtin_functions() { |
969 | | let linter = create_test_linter_with_rules("undefined"); |
970 | | let println_expr = create_test_expr_identifier("println"); |
971 | | let print_expr = create_test_expr_identifier("print"); |
972 | | let eprintln_expr = create_test_expr_identifier("eprintln"); |
973 | | |
974 | | assert_eq!(linter.lint(&println_expr, "println").unwrap().len(), 0); |
975 | | assert_eq!(linter.lint(&print_expr, "print").unwrap().len(), 0); |
976 | | assert_eq!(linter.lint(&eprintln_expr, "eprintln").unwrap().len(), 0); |
977 | | } |
978 | | |
979 | | #[test] |
980 | | fn test_lint_unused_variable() { |
981 | | let linter = create_test_linter_with_rules("unused"); |
982 | | let expr = create_test_expr_let( |
983 | | "x", |
984 | | create_test_expr_literal_int(42), |
985 | | create_test_expr_literal_int(0) |
986 | | ); |
987 | | |
988 | | let issues = linter.lint(&expr, "let x = 42; 0").unwrap(); |
989 | | assert!(issues.iter().any(|i| i.rule == "unused_variable" && i.name == "x")); |
990 | | } |
991 | | |
992 | | #[test] |
993 | | fn test_lint_used_variable() { |
994 | | let linter = create_test_linter_with_rules("unused"); |
995 | | let expr = create_test_expr_let( |
996 | | "x", |
997 | | create_test_expr_literal_int(42), |
998 | | create_test_expr_identifier("x") |
999 | | ); |
1000 | | |
1001 | | let issues = linter.lint(&expr, "let x = 42; x").unwrap(); |
1002 | | assert!(!issues.iter().any(|i| i.rule == "unused_variable" && i.name == "x")); |
1003 | | } |
1004 | | |
1005 | | #[test] |
1006 | | fn test_lint_variable_shadowing() { |
1007 | | let linter = create_test_linter_with_rules("shadowing"); |
1008 | | |
1009 | | // Direct scope test - this should trigger shadowing |
1010 | | let mut parent_scope = Scope::new(); |
1011 | | parent_scope.define("x".to_string(), 1, 1, VarType::Local); |
1012 | | let child_scope = Scope::with_parent(parent_scope); |
1013 | | assert!(child_scope.is_shadowing("x")); |
1014 | | |
1015 | | // Direct test without function wrapper |
1016 | | let outer_let = create_test_expr_let( |
1017 | | "x", |
1018 | | create_test_expr_literal_int(1), |
1019 | | create_test_expr_let( |
1020 | | "x", // This should shadow the outer x |
1021 | | create_test_expr_literal_int(2), |
1022 | | create_test_expr_identifier("x") |
1023 | | ) |
1024 | | ); |
1025 | | |
1026 | | let issues = linter.lint(&outer_let, "let x = 1; let x = 2; x").unwrap(); |
1027 | | eprintln!("Debug - Issues found: {issues:?}"); |
1028 | | assert!(issues.iter().any(|i| i.rule == "shadowing" && i.name == "x")); |
1029 | | } |
1030 | | |
1031 | | // ========== Function Linting Tests ========== |
1032 | | |
1033 | | #[test] |
1034 | | fn test_lint_function_definition() { |
1035 | | let linter = create_test_linter_with_rules("unused"); |
1036 | | let expr = create_test_expr_function( |
1037 | | "test_func", |
1038 | | vec![create_test_param("x")], |
1039 | | create_test_expr_literal_int(42) |
1040 | | ); |
1041 | | |
1042 | | let issues = linter.lint(&expr, "fn test_func(x) { 42 }").unwrap(); |
1043 | | // Parameters are not flagged as unused in function scope analysis |
1044 | | assert!(!issues.iter().any(|i| i.rule == "unused_parameter")); |
1045 | | } |
1046 | | |
1047 | | #[test] |
1048 | | fn test_lint_function_unused_local_variable() { |
1049 | | let linter = create_test_linter_with_rules("unused"); |
1050 | | let body = create_test_expr_let( |
1051 | | "local_var", |
1052 | | create_test_expr_literal_int(1), |
1053 | | create_test_expr_literal_int(42) |
1054 | | ); |
1055 | | let expr = create_test_expr_function( |
1056 | | "test_func", |
1057 | | vec![], |
1058 | | body |
1059 | | ); |
1060 | | |
1061 | | let issues = linter.lint(&expr, "fn test_func() { let local_var = 1; 42 }").unwrap(); |
1062 | | assert!(issues.iter().any(|i| i.rule == "unused_variable" && i.name == "local_var")); |
1063 | | } |
1064 | | |
1065 | | // ========== Loop Linting Tests ========== |
1066 | | |
1067 | | #[test] |
1068 | | fn test_lint_for_loop_unused_variable() { |
1069 | | let linter = create_test_linter_with_rules("unused"); |
1070 | | let expr = create_test_expr_for( |
1071 | | "i", |
1072 | | Some(Pattern::Identifier("i".to_string())), |
1073 | | create_test_expr_literal_int(42), |
1074 | | create_test_expr_literal_int(0) |
1075 | | ); |
1076 | | |
1077 | | let issues = linter.lint(&expr, "for i in items { 0 }").unwrap(); |
1078 | | assert!(issues.iter().any(|i| i.rule.contains("unused") && i.name == "i")); |
1079 | | } |
1080 | | |
1081 | | #[test] |
1082 | | fn test_lint_for_loop_used_variable() { |
1083 | | let linter = create_test_linter_with_rules("unused"); |
1084 | | let expr = create_test_expr_for( |
1085 | | "i", |
1086 | | Some(Pattern::Identifier("i".to_string())), |
1087 | | create_test_expr_literal_int(42), |
1088 | | create_test_expr_identifier("i") |
1089 | | ); |
1090 | | |
1091 | | let issues = linter.lint(&expr, "for i in items { i }").unwrap(); |
1092 | | assert!(!issues.iter().any(|i| i.rule.contains("unused") && i.name == "i")); |
1093 | | } |
1094 | | |
1095 | | #[test] |
1096 | | fn test_lint_for_loop_underscore_variable() { |
1097 | | let linter = create_test_linter_with_rules("unused"); |
1098 | | let expr = create_test_expr_for( |
1099 | | "_", |
1100 | | Some(Pattern::Identifier("_".to_string())), |
1101 | | create_test_expr_literal_int(42), |
1102 | | create_test_expr_literal_int(0) |
1103 | | ); |
1104 | | |
1105 | | let issues = linter.lint(&expr, "for _ in items { 0 }").unwrap(); |
1106 | | assert!(!issues.iter().any(|i| i.name == "_")); |
1107 | | } |
1108 | | |
1109 | | // ========== Match Expression Tests ========== |
1110 | | |
1111 | | #[test] |
1112 | | fn test_lint_match_unused_binding() { |
1113 | | let linter = create_test_linter_with_rules("unused"); |
1114 | | let arm = create_test_match_arm( |
1115 | | Pattern::Identifier("x".to_string()), |
1116 | | create_test_expr_literal_int(42) |
1117 | | ); |
1118 | | let expr = create_test_expr_match( |
1119 | | create_test_expr_literal_int(1), |
1120 | | vec![arm] |
1121 | | ); |
1122 | | |
1123 | | let issues = linter.lint(&expr, "match value { x => 42 }").unwrap(); |
1124 | | assert!(issues.iter().any(|i| i.rule.contains("unused") && i.name == "x")); |
1125 | | } |
1126 | | |
1127 | | #[test] |
1128 | | fn test_lint_match_used_binding() { |
1129 | | let linter = create_test_linter_with_rules("unused"); |
1130 | | let arm = create_test_match_arm( |
1131 | | Pattern::Identifier("x".to_string()), |
1132 | | create_test_expr_identifier("x") |
1133 | | ); |
1134 | | let expr = create_test_expr_match( |
1135 | | create_test_expr_literal_int(1), |
1136 | | vec![arm] |
1137 | | ); |
1138 | | |
1139 | | let issues = linter.lint(&expr, "match value { x => x }").unwrap(); |
1140 | | assert!(!issues.iter().any(|i| i.rule.contains("unused") && i.name == "x")); |
1141 | | } |
1142 | | |
1143 | | #[test] |
1144 | | fn test_lint_match_underscore_binding() { |
1145 | | let linter = create_test_linter_with_rules("unused"); |
1146 | | let arm = create_test_match_arm( |
1147 | | Pattern::Identifier("_".to_string()), |
1148 | | create_test_expr_literal_int(42) |
1149 | | ); |
1150 | | let expr = create_test_expr_match( |
1151 | | create_test_expr_literal_int(1), |
1152 | | vec![arm] |
1153 | | ); |
1154 | | |
1155 | | let issues = linter.lint(&expr, "match value { _ => 42 }").unwrap(); |
1156 | | assert!(!issues.iter().any(|i| i.name == "_")); |
1157 | | } |
1158 | | |
1159 | | // ========== Lambda Expression Tests ========== |
1160 | | |
1161 | | #[test] |
1162 | | fn test_lint_lambda_unused_parameter() { |
1163 | | let linter = create_test_linter_with_rules("unused"); |
1164 | | let expr = create_test_expr_lambda( |
1165 | | vec![create_test_param("x")], |
1166 | | create_test_expr_literal_int(42) |
1167 | | ); |
1168 | | |
1169 | | let issues = linter.lint(&expr, "|x| 42").unwrap(); |
1170 | | assert!(issues.iter().any(|i| i.rule.contains("unused") && i.name == "x")); |
1171 | | } |
1172 | | |
1173 | | #[test] |
1174 | | fn test_lint_lambda_used_parameter() { |
1175 | | let linter = create_test_linter_with_rules("unused"); |
1176 | | let expr = create_test_expr_lambda( |
1177 | | vec![create_test_param("x")], |
1178 | | create_test_expr_identifier("x") |
1179 | | ); |
1180 | | |
1181 | | let issues = linter.lint(&expr, "|x| x").unwrap(); |
1182 | | assert!(!issues.iter().any(|i| i.rule.contains("unused") && i.name == "x")); |
1183 | | } |
1184 | | |
1185 | | // ========== Complexity Tests ========== |
1186 | | |
1187 | | #[test] |
1188 | | fn test_complexity_calculation_simple() { |
1189 | | let linter = create_test_linter(); |
1190 | | let expr = create_test_expr_literal_int(42); |
1191 | | assert_eq!(linter.calculate_complexity(&expr), 0); |
1192 | | } |
1193 | | |
1194 | | #[test] |
1195 | | fn test_complexity_calculation_if() { |
1196 | | let linter = create_test_linter(); |
1197 | | let expr = create_test_expr_if( |
1198 | | create_test_expr_literal_int(1), |
1199 | | create_test_expr_literal_int(2), |
1200 | | Some(create_test_expr_literal_int(3)) |
1201 | | ); |
1202 | | assert_eq!(linter.calculate_complexity(&expr), 1); |
1203 | | } |
1204 | | |
1205 | | #[test] |
1206 | | fn test_complexity_calculation_match() { |
1207 | | let linter = create_test_linter(); |
1208 | | let arm = create_test_match_arm( |
1209 | | Pattern::Identifier("_".to_string()), |
1210 | | create_test_expr_literal_int(42) |
1211 | | ); |
1212 | | let expr = create_test_expr_match( |
1213 | | create_test_expr_literal_int(1), |
1214 | | vec![arm] |
1215 | | ); |
1216 | | assert_eq!(linter.calculate_complexity(&expr), 2); |
1217 | | } |
1218 | | |
1219 | | #[test] |
1220 | | fn test_complexity_calculation_while() { |
1221 | | let linter = create_test_linter(); |
1222 | | let expr = create_test_expr_while( |
1223 | | create_test_expr_literal_int(1), |
1224 | | create_test_expr_literal_int(2) |
1225 | | ); |
1226 | | assert_eq!(linter.calculate_complexity(&expr), 2); |
1227 | | } |
1228 | | |
1229 | | #[test] |
1230 | | fn test_complexity_calculation_for() { |
1231 | | let linter = create_test_linter(); |
1232 | | let expr = create_test_expr_for( |
1233 | | "i", |
1234 | | Some(Pattern::Identifier("i".to_string())), |
1235 | | create_test_expr_literal_int(42), |
1236 | | create_test_expr_literal_int(0) |
1237 | | ); |
1238 | | assert_eq!(linter.calculate_complexity(&expr), 2); |
1239 | | } |
1240 | | |
1241 | | #[test] |
1242 | | fn test_complexity_limit_violation() { |
1243 | | let mut linter = create_test_linter_with_rules("complexity"); |
1244 | | linter.max_complexity = 1; // Very low limit |
1245 | | |
1246 | | let complex_expr = create_test_expr_if( |
1247 | | create_test_expr_literal_int(1), |
1248 | | create_test_expr_if( |
1249 | | create_test_expr_literal_int(2), |
1250 | | create_test_expr_literal_int(3), |
1251 | | None |
1252 | | ), |
1253 | | None |
1254 | | ); |
1255 | | |
1256 | | let issues = linter.lint(&complex_expr, "if 1 { if 2 { 3 } }").unwrap(); |
1257 | | assert!(issues.iter().any(|i| i.rule == "complexity")); |
1258 | | } |
1259 | | |
1260 | | #[test] |
1261 | | fn test_complexity_limit_strict_mode() { |
1262 | | let mut linter = create_test_linter_with_rules("complexity"); |
1263 | | linter.set_strict_mode(true); |
1264 | | linter.max_complexity = 0; |
1265 | | |
1266 | | let expr = create_test_expr_literal_int(42); |
1267 | | let issues = linter.lint(&expr, "42").unwrap(); |
1268 | | // Simple expression should not trigger complexity |
1269 | | assert!(!issues.iter().any(|i| i.rule == "complexity")); |
1270 | | } |
1271 | | |
1272 | | // ========== Pattern Extraction Tests ========== |
1273 | | |
1274 | | #[test] |
1275 | | fn test_extract_loop_bindings_tuple() { |
1276 | | let linter = create_test_linter(); |
1277 | | let mut scope = Scope::new(); |
1278 | | let pattern = Pattern::Tuple(vec![ |
1279 | | Pattern::Identifier("x".to_string()), |
1280 | | Pattern::Identifier("y".to_string()) |
1281 | | ]); |
1282 | | |
1283 | | linter.extract_loop_bindings(&pattern, &mut scope); |
1284 | | assert!(scope.is_defined("x")); |
1285 | | assert!(scope.is_defined("y")); |
1286 | | } |
1287 | | |
1288 | | #[test] |
1289 | | fn test_extract_loop_bindings_list() { |
1290 | | let linter = create_test_linter(); |
1291 | | let mut scope = Scope::new(); |
1292 | | let pattern = Pattern::List(vec![ |
1293 | | Pattern::Identifier("first".to_string()), |
1294 | | Pattern::Identifier("second".to_string()) |
1295 | | ]); |
1296 | | |
1297 | | linter.extract_loop_bindings(&pattern, &mut scope); |
1298 | | assert!(scope.is_defined("first")); |
1299 | | assert!(scope.is_defined("second")); |
1300 | | } |
1301 | | |
1302 | | #[test] |
1303 | | fn test_extract_loop_bindings_struct() { |
1304 | | let linter = create_test_linter(); |
1305 | | let mut scope = Scope::new(); |
1306 | | let pattern = Pattern::Struct { |
1307 | | name: "Point".to_string(), |
1308 | | fields: vec![ |
1309 | | StructPatternField { |
1310 | | name: "x".to_string(), |
1311 | | pattern: Some(Pattern::Identifier("x_val".to_string())), |
1312 | | }, |
1313 | | StructPatternField { |
1314 | | name: "y".to_string(), |
1315 | | pattern: None, |
1316 | | }, |
1317 | | ], |
1318 | | has_rest: false, |
1319 | | }; |
1320 | | |
1321 | | linter.extract_loop_bindings(&pattern, &mut scope); |
1322 | | assert!(scope.is_defined("x_val")); |
1323 | | assert!(scope.is_defined("y")); |
1324 | | } |
1325 | | |
1326 | | #[test] |
1327 | | fn test_extract_param_bindings_underscore() { |
1328 | | let linter = create_test_linter(); |
1329 | | let mut scope = Scope::new(); |
1330 | | let pattern = Pattern::Identifier("_".to_string()); |
1331 | | |
1332 | | linter.extract_param_bindings(&pattern, &mut scope); |
1333 | | assert!(!scope.is_defined("_")); |
1334 | | } |
1335 | | |
1336 | | #[test] |
1337 | | fn test_extract_pattern_bindings_nested_option() { |
1338 | | let linter = create_test_linter(); |
1339 | | let mut scope = Scope::new(); |
1340 | | let pattern = Pattern::Some(Box::new(Pattern::Identifier("value".to_string()))); |
1341 | | |
1342 | | linter.extract_pattern_bindings(&pattern, &mut scope); |
1343 | | assert!(scope.is_defined("value")); |
1344 | | } |
1345 | | |
1346 | | #[test] |
1347 | | fn test_extract_pattern_bindings_ok_err() { |
1348 | | let linter = create_test_linter(); |
1349 | | let mut scope = Scope::new(); |
1350 | | |
1351 | | let ok_pattern = Pattern::Ok(Box::new(Pattern::Identifier("success".to_string()))); |
1352 | | linter.extract_pattern_bindings(&ok_pattern, &mut scope); |
1353 | | assert!(scope.is_defined("success")); |
1354 | | |
1355 | | let err_pattern = Pattern::Err(Box::new(Pattern::Identifier("error".to_string()))); |
1356 | | linter.extract_pattern_bindings(&err_pattern, &mut scope); |
1357 | | assert!(scope.is_defined("error")); |
1358 | | } |
1359 | | |
1360 | | // ========== Expression Analysis Tests ========== |
1361 | | |
1362 | | #[test] |
1363 | | fn test_analyze_binary_expression() { |
1364 | | let linter = create_test_linter_with_rules("undefined"); |
1365 | | let expr = create_test_expr_binary( |
1366 | | BinaryOp::Add, |
1367 | | create_test_expr_identifier("undefined_left"), |
1368 | | create_test_expr_identifier("undefined_right") |
1369 | | ); |
1370 | | |
1371 | | let issues = linter.lint(&expr, "undefined_left + undefined_right").unwrap(); |
1372 | | assert_eq!(issues.len(), 2); |
1373 | | assert!(issues.iter().any(|i| i.name == "undefined_left")); |
1374 | | assert!(issues.iter().any(|i| i.name == "undefined_right")); |
1375 | | } |
1376 | | |
1377 | | #[test] |
1378 | | fn test_analyze_call_expression() { |
1379 | | let linter = create_test_linter_with_rules("undefined"); |
1380 | | let expr = create_test_expr_call( |
1381 | | create_test_expr_identifier("undefined_func"), |
1382 | | vec![create_test_expr_identifier("undefined_arg")] |
1383 | | ); |
1384 | | |
1385 | | let issues = linter.lint(&expr, "undefined_func(undefined_arg)").unwrap(); |
1386 | | assert_eq!(issues.len(), 2); |
1387 | | assert!(issues.iter().any(|i| i.name == "undefined_func")); |
1388 | | assert!(issues.iter().any(|i| i.name == "undefined_arg")); |
1389 | | } |
1390 | | |
1391 | | #[test] |
1392 | | fn test_analyze_method_call_expression() { |
1393 | | let linter = create_test_linter_with_rules("undefined"); |
1394 | | let expr = create_test_expr_method_call( |
1395 | | create_test_expr_identifier("undefined_obj"), |
1396 | | "method", |
1397 | | vec![create_test_expr_identifier("undefined_arg")] |
1398 | | ); |
1399 | | |
1400 | | let issues = linter.lint(&expr, "undefined_obj.method(undefined_arg)").unwrap(); |
1401 | | assert_eq!(issues.len(), 2); |
1402 | | assert!(issues.iter().any(|i| i.name == "undefined_obj")); |
1403 | | assert!(issues.iter().any(|i| i.name == "undefined_arg")); |
1404 | | } |
1405 | | |
1406 | | #[test] |
1407 | | fn test_analyze_string_interpolation() { |
1408 | | let linter = create_test_linter_with_rules("undefined"); |
1409 | | let expr = Expr::new(ExprKind::StringInterpolation { |
1410 | | parts: vec![ |
1411 | | StringPart::Text("Hello ".to_string()), |
1412 | | StringPart::Expr(Box::new(create_test_expr_identifier("undefined_name"))), |
1413 | | StringPart::ExprWithFormat { |
1414 | | expr: Box::new(create_test_expr_identifier("undefined_age")), |
1415 | | format_spec: "d".to_string() |
1416 | | }, |
1417 | | ] |
1418 | | }, create_test_span()); |
1419 | | |
1420 | | let issues = linter.lint(&expr, "f\"Hello {undefined_name} {undefined_age:d}\"").unwrap(); |
1421 | | assert_eq!(issues.len(), 2); |
1422 | | assert!(issues.iter().any(|i| i.name == "undefined_name")); |
1423 | | assert!(issues.iter().any(|i| i.name == "undefined_age")); |
1424 | | } |
1425 | | |
1426 | | #[test] |
1427 | | fn test_analyze_return_expression() { |
1428 | | let linter = create_test_linter_with_rules("undefined"); |
1429 | | let expr = create_test_expr_return(Some(create_test_expr_identifier("undefined_var"))); |
1430 | | |
1431 | | let issues = linter.lint(&expr, "return undefined_var").unwrap(); |
1432 | | assert_eq!(issues.len(), 1); |
1433 | | assert!(issues.iter().any(|i| i.name == "undefined_var")); |
1434 | | } |
1435 | | |
1436 | | #[test] |
1437 | | fn test_analyze_list_and_tuple() { |
1438 | | let linter = create_test_linter_with_rules("undefined"); |
1439 | | |
1440 | | let list_expr = Expr::new(ExprKind::List(vec![ |
1441 | | create_test_expr_identifier("undefined_item") |
1442 | | ]), create_test_span()); |
1443 | | |
1444 | | let tuple_expr = Expr::new(ExprKind::Tuple(vec![ |
1445 | | create_test_expr_identifier("undefined_elem") |
1446 | | ]), create_test_span()); |
1447 | | |
1448 | | let list_issues = linter.lint(&list_expr, "[undefined_item]").unwrap(); |
1449 | | assert!(list_issues.iter().any(|i| i.name == "undefined_item")); |
1450 | | |
1451 | | let tuple_issues = linter.lint(&tuple_expr, "(undefined_elem,)").unwrap(); |
1452 | | assert!(tuple_issues.iter().any(|i| i.name == "undefined_elem")); |
1453 | | } |
1454 | | |
1455 | | #[test] |
1456 | | fn test_analyze_field_and_index_access() { |
1457 | | let linter = create_test_linter_with_rules("undefined"); |
1458 | | |
1459 | | let field_expr = Expr::new(ExprKind::FieldAccess { |
1460 | | object: Box::new(create_test_expr_identifier("undefined_obj")), |
1461 | | field: "property".to_string(), |
1462 | | }, create_test_span()); |
1463 | | |
1464 | | let index_expr = Expr::new(ExprKind::IndexAccess { |
1465 | | object: Box::new(create_test_expr_identifier("undefined_arr")), |
1466 | | index: Box::new(create_test_expr_identifier("undefined_idx")), |
1467 | | }, create_test_span()); |
1468 | | |
1469 | | let field_issues = linter.lint(&field_expr, "undefined_obj.property").unwrap(); |
1470 | | assert!(field_issues.iter().any(|i| i.name == "undefined_obj")); |
1471 | | |
1472 | | let index_issues = linter.lint(&index_expr, "undefined_arr[undefined_idx]").unwrap(); |
1473 | | assert_eq!(index_issues.len(), 2); |
1474 | | assert!(index_issues.iter().any(|i| i.name == "undefined_arr")); |
1475 | | assert!(index_issues.iter().any(|i| i.name == "undefined_idx")); |
1476 | | } |
1477 | | |
1478 | | #[test] |
1479 | | fn test_analyze_assign_expression() { |
1480 | | let linter = create_test_linter_with_rules("undefined"); |
1481 | | let expr = Expr::new(ExprKind::Assign { |
1482 | | target: Box::new(create_test_expr_identifier("undefined_target")), |
1483 | | value: Box::new(create_test_expr_identifier("undefined_value")), |
1484 | | }, create_test_span()); |
1485 | | |
1486 | | let issues = linter.lint(&expr, "undefined_target = undefined_value").unwrap(); |
1487 | | assert_eq!(issues.len(), 2); |
1488 | | assert!(issues.iter().any(|i| i.name == "undefined_target")); |
1489 | | assert!(issues.iter().any(|i| i.name == "undefined_value")); |
1490 | | } |
1491 | | |
1492 | | // ========== Block Scope Tests ========== |
1493 | | |
1494 | | #[test] |
1495 | | fn test_analyze_block_unused_variable() { |
1496 | | let linter = create_test_linter_with_rules("unused"); |
1497 | | let block = create_test_expr_block(vec![ |
1498 | | create_test_expr_let("unused_var", create_test_expr_literal_int(42), create_test_expr_literal_int(0)) |
1499 | | ]); |
1500 | | |
1501 | | let issues = linter.lint(&block, "{ let unused_var = 42; 0 }").unwrap(); |
1502 | | assert!(issues.iter().any(|i| i.rule == "unused_variable" && i.name == "unused_var")); |
1503 | | } |
1504 | | |
1505 | | #[test] |
1506 | | fn test_analyze_if_branches() { |
1507 | | let linter = create_test_linter_with_rules("undefined"); |
1508 | | let expr = create_test_expr_if( |
1509 | | create_test_expr_identifier("undefined_cond"), |
1510 | | create_test_expr_identifier("undefined_then"), |
1511 | | Some(create_test_expr_identifier("undefined_else")) |
1512 | | ); |
1513 | | |
1514 | | let issues = linter.lint(&expr, "if undefined_cond { undefined_then } else { undefined_else }").unwrap(); |
1515 | | assert_eq!(issues.len(), 3); |
1516 | | assert!(issues.iter().any(|i| i.name == "undefined_cond")); |
1517 | | assert!(issues.iter().any(|i| i.name == "undefined_then")); |
1518 | | assert!(issues.iter().any(|i| i.name == "undefined_else")); |
1519 | | } |
1520 | | |
1521 | | // ========== Auto-fix Tests ========== |
1522 | | |
1523 | | #[test] |
1524 | | fn test_auto_fix_style_issue() { |
1525 | | let linter = create_test_linter(); |
1526 | | let issues = vec![LintIssue { |
1527 | | line: 1, |
1528 | | column: 1, |
1529 | | severity: "warning".to_string(), |
1530 | | rule: "style".to_string(), |
1531 | | message: "double spaces".to_string(), |
1532 | | suggestion: "Use single spaces".to_string(), |
1533 | | issue_type: "style".to_string(), |
1534 | | name: "spacing".to_string(), |
1535 | | }]; |
1536 | | |
1537 | | let fixed = linter.auto_fix("let x = 42", &issues).unwrap(); |
1538 | | assert_eq!(fixed, "let x = 42"); |
1539 | | } |
1540 | | |
1541 | | #[test] |
1542 | | fn test_auto_fix_no_issues() { |
1543 | | let linter = create_test_linter(); |
1544 | | let issues = vec![]; |
1545 | | |
1546 | | let fixed = linter.auto_fix("let x = 42", &issues).unwrap(); |
1547 | | assert_eq!(fixed, "let x = 42"); |
1548 | | } |
1549 | | |
1550 | | // ========== Integration Tests ========== |
1551 | | |
1552 | | #[test] |
1553 | | fn test_comprehensive_linting() { |
1554 | | let linter = create_test_linter_with_rules("unused,undefined,shadowing"); |
1555 | | |
1556 | | // Create nested let expressions for comprehensive testing |
1557 | | let unused_let = create_test_expr_let( |
1558 | | "unused", |
1559 | | create_test_expr_identifier("undefined"), // This creates undefined variable |
1560 | | create_test_expr_identifier("x") |
1561 | | ); |
1562 | | let shadow_let = create_test_expr_let( |
1563 | | "x", // This shadows the outer x |
1564 | | create_test_expr_literal_int(2), |
1565 | | unused_let |
1566 | | ); |
1567 | | let outer_let = create_test_expr_let( |
1568 | | "x", // Outer variable |
1569 | | create_test_expr_literal_int(1), |
1570 | | shadow_let |
1571 | | ); |
1572 | | |
1573 | | let issues = linter.lint(&outer_let, "complex code").unwrap(); |
1574 | | assert!(issues.iter().any(|i| i.rule == "shadowing")); |
1575 | | assert!(issues.iter().any(|i| i.rule == "undefined")); |
1576 | | assert!(issues.iter().any(|i| i.rule == "unused_variable")); |
1577 | | } |
1578 | | |
1579 | | #[test] |
1580 | | fn test_variable_type_classification() { |
1581 | | let var_info = VariableInfo { |
1582 | | defined_at: (1, 1), |
1583 | | used: false, |
1584 | | var_type: VarType::Parameter, |
1585 | | }; |
1586 | | |
1587 | | assert_eq!(var_info.defined_at, (1, 1)); |
1588 | | assert!(!var_info.used); |
1589 | | assert!(matches!(var_info.var_type, VarType::Parameter)); |
1590 | | } |
1591 | | |
1592 | | #[test] |
1593 | | fn test_empty_issues_json_compatibility() { |
1594 | | let linter = create_test_linter(); |
1595 | | let expr = create_test_expr_literal_int(42); |
1596 | | |
1597 | | let issues = linter.lint(&expr, "42").unwrap(); |
1598 | | assert_eq!(issues.len(), 0); |
1599 | | |
1600 | | let json = serde_json::to_string(&issues).unwrap(); |
1601 | | assert_eq!(json, "[]"); |
1602 | | } |
1603 | | } |