Coverage Report

Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/ruchy/src/lints/mod.rs
Line
Count
Source
1
/// Custom lint rules for Ruchy code quality
2
use crate::frontend::ast::{Expr, ExprKind};
3
use thiserror::Error;
4
5
#[derive(Debug, Error)]
6
pub enum LintViolation {
7
    #[error("{location}: {message} (severity: {severity:?})")]
8
    Violation {
9
        location: String,
10
        message: String,
11
        severity: Severity,
12
        suggestion: Option<String>,
13
    },
14
}
15
16
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17
pub enum Severity {
18
    Error,
19
    Warning,
20
    Info,
21
}
22
23
/// Trait for implementing lint rules
24
pub trait LintRule: Send + Sync {
25
    fn name(&self) -> &str;
26
    fn check_expression(&self, expr: &Expr) -> Vec<LintViolation>;
27
}
28
29
/// Main linter that runs all rules
30
pub struct RuchyLinter {
31
    rules: Vec<Box<dyn LintRule>>,
32
}
33
34
impl Default for RuchyLinter {
35
0
    fn default() -> Self {
36
0
        Self::new()
37
0
    }
38
}
39
40
impl RuchyLinter {
41
0
    pub fn new() -> Self {
42
0
        let rules: Vec<Box<dyn LintRule>> = vec![
43
0
            Box::new(ComplexityRule::default()),
44
0
            Box::new(NoDebugPrintRule),
45
        ];
46
47
0
        Self { rules }
48
0
    }
49
50
0
    pub fn add_rule(&mut self, rule: Box<dyn LintRule>) {
51
0
        self.rules.push(rule);
52
0
    }
53
54
0
    pub fn lint(&self, expr: &Expr) -> Vec<LintViolation> {
55
0
        let mut violations = Vec::new();
56
57
0
        for rule in &self.rules {
58
0
            violations.extend(rule.check_expression(expr));
59
0
        }
60
61
0
        violations
62
0
    }
63
}
64
65
/// Rule: Cyclomatic complexity limit
66
#[derive(Default)]
67
struct ComplexityRule {
68
    max_complexity: usize,
69
}
70
71
impl ComplexityRule {
72
    #[allow(clippy::only_used_in_recursion)]
73
0
    fn calculate_complexity(&self, expr: &Expr) -> usize {
74
0
        match &expr.kind {
75
            ExprKind::If {
76
0
                condition,
77
0
                then_branch,
78
0
                else_branch,
79
            } => {
80
0
                1 + self.calculate_complexity(condition)
81
0
                    + self.calculate_complexity(then_branch)
82
0
                    + else_branch
83
0
                        .as_ref()
84
0
                        .map_or(0, |e| self.calculate_complexity(e))
85
            }
86
0
            ExprKind::Match { expr, arms } => {
87
0
                1 + self.calculate_complexity(expr)
88
0
                    + arms
89
0
                        .iter()
90
0
                        .map(|arm| self.calculate_complexity(&arm.body))
91
0
                        .sum::<usize>()
92
            }
93
0
            ExprKind::While { condition, body } => {
94
0
                1 + self.calculate_complexity(condition) + self.calculate_complexity(body)
95
            }
96
0
            ExprKind::For { iter, body, .. } => {
97
0
                1 + self.calculate_complexity(iter) + self.calculate_complexity(body)
98
            }
99
0
            ExprKind::Binary { left, right, .. } => {
100
0
                self.calculate_complexity(left) + self.calculate_complexity(right)
101
            }
102
0
            _ => 0,
103
        }
104
0
    }
105
}
106
107
impl LintRule for ComplexityRule {
108
0
    fn name(&self) -> &'static str {
109
0
        "complexity"
110
0
    }
111
112
0
    fn check_expression(&self, expr: &Expr) -> Vec<LintViolation> {
113
0
        let mut violations = Vec::new();
114
0
        let max = if self.max_complexity == 0 {
115
0
            10
116
        } else {
117
0
            self.max_complexity
118
        };
119
120
0
        let complexity = self.calculate_complexity(expr);
121
0
        if complexity > max {
122
0
            violations.push(LintViolation::Violation {
123
0
                location: format!("position {}", expr.span.start),
124
0
                message: format!("Cyclomatic complexity is {complexity} (max: {max})"),
125
0
                severity: Severity::Warning,
126
0
                suggestion: Some("Consider breaking this into smaller functions".to_string()),
127
0
            });
128
0
        }
129
130
0
        violations
131
0
    }
132
}
133
134
/// Rule: No debug print statements
135
struct NoDebugPrintRule;
136
137
impl LintRule for NoDebugPrintRule {
138
0
    fn name(&self) -> &'static str {
139
0
        "no_debug_print"
140
0
    }
141
142
0
    fn check_expression(&self, expr: &Expr) -> Vec<LintViolation> {
143
0
        match &expr.kind {
144
0
            ExprKind::Call { func, .. } => {
145
0
                if let ExprKind::Identifier(name) = &func.kind {
146
0
                    if name == "dbg" || name == "debug_print" {
147
0
                        vec![LintViolation::Violation {
148
0
                            location: format!("position {}", expr.span.start),
149
0
                            message: "Debug print statement found".to_string(),
150
0
                            severity: Severity::Warning,
151
0
                            suggestion: Some(
152
0
                                "Remove debug statements before committing".to_string(),
153
0
                            ),
154
0
                        }]
155
                    } else {
156
0
                        vec![]
157
                    }
158
                } else {
159
0
                    vec![]
160
                }
161
            }
162
0
            _ => vec![],
163
        }
164
0
    }
165
}
166
167
#[cfg(test)]
168
mod tests {
169
    use super::*;
170
    use crate::frontend::ast::{Expr, ExprKind, Span, Literal, BinaryOp};
171
    
172
    fn make_test_expr(kind: ExprKind) -> Expr {
173
        Expr {
174
            kind,
175
            span: Span::new(0, 10),
176
            attributes: vec![],
177
        }
178
    }
179
    
180
    #[test]
181
    fn test_linter_creation() {
182
        let linter = RuchyLinter::new();
183
        assert_eq!(linter.rules.len(), 2);
184
    }
185
    
186
    #[test]
187
    fn test_linter_default() {
188
        let linter = RuchyLinter::default();
189
        assert_eq!(linter.rules.len(), 2);
190
    }
191
    
192
    #[test]
193
    fn test_add_custom_rule() {
194
        struct TestRule;
195
        impl LintRule for TestRule {
196
            fn name(&self) -> &'static str {
197
                "test"
198
            }
199
            fn check_expression(&self, _expr: &Expr) -> Vec<LintViolation> {
200
                vec![]
201
            }
202
        }
203
        
204
        let mut linter = RuchyLinter::new();
205
        linter.add_rule(Box::new(TestRule));
206
        assert_eq!(linter.rules.len(), 3);
207
    }
208
    
209
    #[test]
210
    fn test_severity_display() {
211
        assert_eq!(format!("{:?}", Severity::Error), "Error");
212
        assert_eq!(format!("{:?}", Severity::Warning), "Warning");
213
        assert_eq!(format!("{:?}", Severity::Info), "Info");
214
    }
215
    
216
    #[test]
217
    fn test_severity_equality() {
218
        assert_eq!(Severity::Error, Severity::Error);
219
        assert_ne!(Severity::Error, Severity::Warning);
220
    }
221
    
222
    #[test]
223
    fn test_lint_violation_display() {
224
        let violation = LintViolation::Violation {
225
            location: "line 5".to_string(),
226
            message: "Test violation".to_string(),
227
            severity: Severity::Warning,
228
            suggestion: Some("Fix it".to_string()),
229
        };
230
        
231
        let display = violation.to_string();
232
        assert!(display.contains("line 5"));
233
        assert!(display.contains("Test violation"));
234
        assert!(display.contains("Warning"));
235
    }
236
    
237
    #[test]
238
    fn test_lint_violation_without_suggestion() {
239
        let violation = LintViolation::Violation {
240
            location: "position 10".to_string(),
241
            message: "Error found".to_string(),
242
            severity: Severity::Error,
243
            suggestion: None,
244
        };
245
        
246
        let display = violation.to_string();
247
        assert!(display.contains("position 10"));
248
        assert!(display.contains("Error found"));
249
        assert!(display.contains("Error"));
250
    }
251
    
252
    #[test]
253
    fn test_complexity_rule_simple() {
254
        let rule = ComplexityRule::default();
255
        let expr = make_test_expr(ExprKind::Literal(Literal::Integer(42)));
256
        let violations = rule.check_expression(&expr);
257
        assert!(violations.is_empty());
258
    }
259
    
260
    #[test]
261
    fn test_complexity_rule_name() {
262
        let rule = ComplexityRule::default();
263
        assert_eq!(rule.name(), "complexity");
264
    }
265
    
266
    #[test]
267
    fn test_complexity_rule_if_statement() {
268
        let rule = ComplexityRule { max_complexity: 0 }; // Will use default of 10
269
        
270
        // Create a simple if statement
271
        let if_expr = make_test_expr(ExprKind::If {
272
            condition: Box::new(make_test_expr(ExprKind::Literal(Literal::Bool(true)))),
273
            then_branch: Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(1)))),
274
            else_branch: None,
275
        });
276
        
277
        let violations = rule.check_expression(&if_expr);
278
        assert!(violations.is_empty()); // Complexity is 1, under limit of 10
279
    }
280
    
281
    #[test]
282
    fn test_complexity_rule_nested_if_exceeds_limit() {
283
        let rule = ComplexityRule { max_complexity: 1 };
284
        
285
        // Create nested if statements to exceed complexity of 1
286
        let inner_if = make_test_expr(ExprKind::If {
287
            condition: Box::new(make_test_expr(ExprKind::Literal(Literal::Bool(true)))),
288
            then_branch: Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(1)))),
289
            else_branch: None,
290
        });
291
        
292
        let outer_if = make_test_expr(ExprKind::If {
293
            condition: Box::new(make_test_expr(ExprKind::Literal(Literal::Bool(false)))),
294
            then_branch: Box::new(inner_if),
295
            else_branch: Some(Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(2))))),
296
        });
297
        
298
        let violations = rule.check_expression(&outer_if);
299
        assert!(!violations.is_empty());
300
        assert!(violations[0].to_string().contains("Cyclomatic complexity"));
301
    }
302
    
303
    #[test]
304
    fn test_complexity_rule_while_loop() {
305
        let rule = ComplexityRule { max_complexity: 5 };
306
        
307
        let while_expr = make_test_expr(ExprKind::While {
308
            condition: Box::new(make_test_expr(ExprKind::Literal(Literal::Bool(true)))),
309
            body: Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(42)))),
310
        });
311
        
312
        let violations = rule.check_expression(&while_expr);
313
        assert!(violations.is_empty()); // Complexity is 1, under limit
314
    }
315
    
316
    #[test]
317
    fn test_complexity_rule_for_loop() {
318
        let rule = ComplexityRule { max_complexity: 5 };
319
        
320
        let for_expr = make_test_expr(ExprKind::For {
321
            var: "i".to_string(),
322
            pattern: None,
323
            iter: Box::new(make_test_expr(ExprKind::Range {
324
                start: Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(0)))),
325
                end: Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(10)))),
326
                inclusive: false,
327
            })),
328
            body: Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(42)))),
329
        });
330
        
331
        let violations = rule.check_expression(&for_expr);
332
        assert!(violations.is_empty()); // Complexity is 1, under limit
333
    }
334
    
335
    #[test]
336
    fn test_complexity_rule_binary_operation() {
337
        let rule = ComplexityRule { max_complexity: 5 };
338
        
339
        let binary_expr = make_test_expr(ExprKind::Binary {
340
            left: Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(1)))),
341
            op: BinaryOp::Add,
342
            right: Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(2)))),
343
        });
344
        
345
        let violations = rule.check_expression(&binary_expr);
346
        assert!(violations.is_empty()); // Binary operations don't add complexity
347
    }
348
    
349
    #[test]
350
    fn test_no_debug_print_rule() {
351
        let rule = NoDebugPrintRule;
352
        
353
        // Test normal function call - no violation
354
        let normal_call = make_test_expr(ExprKind::Call {
355
            func: Box::new(make_test_expr(ExprKind::Identifier("println".to_string()))),
356
            args: vec![],
357
        });
358
        assert!(rule.check_expression(&normal_call).is_empty());
359
        
360
        // Test debug print - should have violation
361
        let debug_call = make_test_expr(ExprKind::Call {
362
            func: Box::new(make_test_expr(ExprKind::Identifier("dbg".to_string()))),
363
            args: vec![],
364
        });
365
        let violations = rule.check_expression(&debug_call);
366
        assert_eq!(violations.len(), 1);
367
        assert!(violations[0].to_string().contains("Debug print"));
368
        
369
        // Test debug_print - should have violation
370
        let debug_print = make_test_expr(ExprKind::Call {
371
            func: Box::new(make_test_expr(ExprKind::Identifier("debug_print".to_string()))),
372
            args: vec![],
373
        });
374
        let violations = rule.check_expression(&debug_print);
375
        assert_eq!(violations.len(), 1);
376
    }
377
    
378
    #[test]
379
    fn test_no_debug_print_rule_name() {
380
        let rule = NoDebugPrintRule;
381
        assert_eq!(rule.name(), "no_debug_print");
382
    }
383
    
384
    #[test]
385
    fn test_linter_runs_all_rules() {
386
        let linter = RuchyLinter::new();
387
        
388
        // Create expression that violates debug print rule
389
        let expr = make_test_expr(ExprKind::Call {
390
            func: Box::new(make_test_expr(ExprKind::Identifier("dbg".to_string()))),
391
            args: vec![],
392
        });
393
        
394
        let violations = linter.lint(&expr);
395
        assert!(!violations.is_empty());
396
    }
397
    
398
    #[test]
399
    fn test_linter_no_violations() {
400
        let linter = RuchyLinter::new();
401
        
402
        // Create simple expression with no violations
403
        let expr = make_test_expr(ExprKind::Literal(Literal::Integer(42)));
404
        
405
        let violations = linter.lint(&expr);
406
        assert!(violations.is_empty());
407
    }
408
    
409
    #[test]
410
    fn test_complexity_calculation_match() {
411
        let rule = ComplexityRule { max_complexity: 10 };
412
        
413
        // Match expressions add complexity
414
        use crate::frontend::ast::{MatchArm, Pattern};
415
        
416
        let arms = vec![
417
            MatchArm {
418
                pattern: Pattern::Literal(Literal::Integer(1)),
419
                guard: None,
420
                body: Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(10)))),
421
                span: Span::new(0, 10),
422
            },
423
            MatchArm {
424
                pattern: Pattern::Literal(Literal::Integer(2)),
425
                guard: None,
426
                body: Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(20)))),
427
                span: Span::new(0, 10),
428
            },
429
        ];
430
        
431
        let match_expr = make_test_expr(ExprKind::Match {
432
            expr: Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(42)))),
433
            arms,
434
        });
435
        
436
        let violations = rule.check_expression(&match_expr);
437
        assert!(violations.is_empty()); // Under complexity limit
438
    }
439
440
    #[test]
441
    fn test_complexity_rule_with_custom_max() {
442
        let rule = ComplexityRule { max_complexity: 3 };
443
        
444
        // Simple literal should not trigger
445
        let expr = make_test_expr(ExprKind::Literal(Literal::Integer(42)));
446
        let violations = rule.check_expression(&expr);
447
        assert!(violations.is_empty());
448
    }
449
450
    #[test]
451
    fn test_no_debug_print_rule_non_call_expression() {
452
        let rule = NoDebugPrintRule;
453
        
454
        // Test with non-call expression
455
        let expr = make_test_expr(ExprKind::Literal(Literal::String("test".to_string())));
456
        let violations = rule.check_expression(&expr);
457
        assert!(violations.is_empty());
458
    }
459
460
    #[test]
461
    fn test_no_debug_print_rule_non_identifier_function() {
462
        let rule = NoDebugPrintRule;
463
        
464
        // Test with non-identifier function (e.g., lambda call)
465
        let expr = make_test_expr(ExprKind::Call {
466
            func: Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(42)))),
467
            args: vec![],
468
        });
469
        
470
        let violations = rule.check_expression(&expr);
471
        assert!(violations.is_empty());
472
    }
473
474
    #[test]
475
    fn test_lint_violation_debug_formatting() {
476
        let violation = LintViolation::Violation {
477
            location: "test.ruchy:10:5".to_string(),
478
            message: "Complex expression detected".to_string(),
479
            severity: Severity::Info,
480
            suggestion: None,
481
        };
482
        
483
        let debug_str = format!("{violation:?}");
484
        assert!(debug_str.contains("Violation"));
485
        assert!(debug_str.contains("test.ruchy:10:5"));
486
    }
487
488
    #[test]
489
    fn test_severity_clone() {
490
        let sev1 = Severity::Warning;
491
        let sev2 = sev1;
492
        assert_eq!(sev1, sev2);
493
    }
494
495
    #[test]
496
    fn test_complexity_rule_if_with_else() {
497
        let rule = ComplexityRule { max_complexity: 5 };
498
        
499
        let if_expr = make_test_expr(ExprKind::If {
500
            condition: Box::new(make_test_expr(ExprKind::Literal(Literal::Bool(true)))),
501
            then_branch: Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(1)))),
502
            else_branch: Some(Box::new(make_test_expr(ExprKind::Literal(Literal::Integer(2))))),
503
        });
504
        
505
        let violations = rule.check_expression(&if_expr);
506
        assert!(violations.is_empty());
507
    }
508
509
    #[test]
510
    fn test_multiple_violations_from_linter() {
511
        let mut linter = RuchyLinter::new();
512
        
513
        // Add another rule that always fails
514
        struct AlwaysFailRule;
515
        impl LintRule for AlwaysFailRule {
516
            fn name(&self) -> &'static str {
517
                "always_fail"
518
            }
519
            fn check_expression(&self, expr: &Expr) -> Vec<LintViolation> {
520
                vec![LintViolation::Violation {
521
                    location: format!("position {}", expr.span.start),
522
                    message: "Always fails".to_string(),
523
                    severity: Severity::Error,
524
                    suggestion: None,
525
                }]
526
            }
527
        }
528
        
529
        linter.add_rule(Box::new(AlwaysFailRule));
530
        
531
        // Create expression that also violates debug print rule
532
        let expr = make_test_expr(ExprKind::Call {
533
            func: Box::new(make_test_expr(ExprKind::Identifier("dbg".to_string()))),
534
            args: vec![],
535
        });
536
        
537
        let violations = linter.lint(&expr);
538
        assert!(violations.len() >= 2); // At least 2 violations
539
    }
540
}