Coverage Report

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