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/quality/scoring.rs
Line
Count
Source
1
//! Unified quality scoring system for Ruchy code (RUCHY-0810)
2
//! Incremental scoring architecture (RUCHY-0813)
3
4
use std::collections::HashMap;
5
use std::time::{Duration, SystemTime};
6
use std::path::PathBuf;
7
use std::fs;
8
use crate::frontend::ast::ExprKind;
9
10
/// Analysis depth for quality scoring
11
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
12
pub enum AnalysisDepth {
13
    /// <100ms - AST metrics only
14
    Shallow,
15
    /// <1s - AST + type checking + basic flow
16
    Standard,
17
    /// <30s - Full property/mutation testing
18
    Deep,
19
}
20
21
/// Unified quality score with components
22
#[derive(Debug, Clone)]
23
pub struct QualityScore {
24
    pub value: f64,           // 0.0-1.0 normalized score
25
    pub components: ScoreComponents,
26
    pub grade: Grade,         // Human-readable grade
27
    pub confidence: f64,      // Confidence in score accuracy
28
    pub cache_hit_rate: f64,  // Percentage from cached analysis
29
}
30
31
/// Individual score components
32
#[derive(Debug, Clone)]
33
pub struct ScoreComponents {
34
    pub correctness: f64,     // 35% - Semantic correctness
35
    pub performance: f64,     // 25% - Runtime efficiency  
36
    pub maintainability: f64, // 20% - Change resilience
37
    pub safety: f64,          // 15% - Memory/type safety
38
    pub idiomaticity: f64,    // 5%  - Language conventions
39
}
40
41
/// Human-readable grade boundaries
42
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
43
pub enum Grade {
44
    APlus,  // [0.97, 1.00] - Ship to production
45
    A,      // [0.93, 0.97) - Ship with confidence
46
    AMinus, // [0.90, 0.93) - Ship with review
47
    BPlus,  // [0.87, 0.90) - Acceptable
48
    B,      // [0.83, 0.87) - Needs work
49
    BMinus, // [0.80, 0.83) - Minimum viable
50
    CPlus,  // [0.77, 0.80) - Technical debt
51
    C,      // [0.73, 0.77) - Refactor advised
52
    CMinus, // [0.70, 0.73) - Refactor required
53
    D,      // [0.60, 0.70) - Major issues
54
    F,      // [0.00, 0.60) - Fundamental problems
55
}
56
57
impl Grade {
58
24
    pub fn from_score(value: f64) -> Self {
59
24
        match value {
60
24
            
v23
if v >= 0.97 =>
Grade::APlus23
,
61
1
            v if v >= 0.93 => Grade::A,
62
0
            v if v >= 0.90 => Grade::AMinus,
63
0
            v if v >= 0.87 => Grade::BPlus,
64
0
            v if v >= 0.83 => Grade::B,
65
0
            v if v >= 0.80 => Grade::BMinus,
66
0
            v if v >= 0.77 => Grade::CPlus,
67
0
            v if v >= 0.73 => Grade::C,
68
0
            v if v >= 0.70 => Grade::CMinus,
69
0
            v if v >= 0.60 => Grade::D,
70
0
            _ => Grade::F,
71
        }
72
24
    }
73
}
74
75
impl std::fmt::Display for Grade {
76
24
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77
24
        match self {
78
19
            Grade::APlus => write!(f, "A+"),
79
1
            Grade::A => write!(f, "A"),
80
0
            Grade::AMinus => write!(f, "A-"),
81
0
            Grade::BPlus => write!(f, "B+"),
82
0
            Grade::B => write!(f, "B"),
83
2
            Grade::BMinus => write!(f, "B-"),
84
0
            Grade::CPlus => write!(f, "C+"),
85
0
            Grade::C => write!(f, "C"),
86
0
            Grade::CMinus => write!(f, "C-"),
87
2
            Grade::D => write!(f, "D"),
88
0
            Grade::F => write!(f, "F"),
89
        }
90
24
    }
91
}
92
93
/// Configuration for score weights
94
#[derive(Debug, Clone)]
95
pub struct ScoreConfig {
96
    pub correctness_weight: f64,
97
    pub performance_weight: f64,
98
    pub maintainability_weight: f64,
99
    pub safety_weight: f64,
100
    pub idiomaticity_weight: f64,
101
}
102
103
impl Default for ScoreConfig {
104
24
    fn default() -> Self {
105
24
        Self {
106
24
            correctness_weight: 0.35,
107
24
            performance_weight: 0.25,
108
24
            maintainability_weight: 0.20,
109
24
            safety_weight: 0.15,
110
24
            idiomaticity_weight: 0.05,
111
24
        }
112
24
    }
113
}
114
115
/// Cache key for scoring results
116
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
117
pub struct CacheKey {
118
    pub file_path: PathBuf,
119
    pub content_hash: u64,
120
    pub depth: AnalysisDepth,
121
}
122
123
/// Cached scoring result with metadata
124
#[derive(Debug, Clone)]
125
pub struct CacheEntry {
126
    pub score: QualityScore,
127
    pub timestamp: SystemTime,
128
    pub dependencies: Vec<PathBuf>,
129
}
130
131
/// File dependency tracker
132
#[derive(Debug)]
133
pub struct DependencyTracker {
134
    /// Map of file -> files it depends on
135
    dependencies: HashMap<PathBuf, Vec<PathBuf>>,
136
    /// Map of file -> last modified time
137
    file_times: HashMap<PathBuf, SystemTime>,
138
}
139
140
impl Default for DependencyTracker {
141
0
    fn default() -> Self {
142
0
        Self::new()
143
0
    }
144
}
145
146
impl DependencyTracker {
147
24
    pub fn new() -> Self {
148
24
        Self {
149
24
            dependencies: HashMap::new(),
150
24
            file_times: HashMap::new(),
151
24
        }
152
24
    }
153
154
0
    pub fn track_dependency(&mut self, file: PathBuf, dependency: PathBuf) {
155
0
        self.dependencies.entry(file).or_default().push(dependency);
156
0
    }
157
158
0
    pub fn is_stale(&self, file: &PathBuf) -> bool {
159
0
        if let Some(dependencies) = self.dependencies.get(file) {
160
0
            for dep in dependencies {
161
0
                if self.is_file_modified(dep) {
162
0
                    return true;
163
0
                }
164
            }
165
0
        }
166
0
        false
167
0
    }
168
169
0
    fn is_file_modified(&self, file: &PathBuf) -> bool {
170
0
        let Ok(metadata) = fs::metadata(file) else { return true; };
171
0
        let Ok(modified) = metadata.modified() else { return true; };
172
        
173
0
        if let Some(&cached_time) = self.file_times.get(file) {
174
0
            modified > cached_time
175
        } else {
176
0
            true
177
        }
178
0
    }
179
180
24
    pub fn update_file_time(&mut self, file: PathBuf) {
181
24
        if let Ok(metadata) = fs::metadata(&file) {
182
24
            if let Ok(modified) = metadata.modified() {
183
24
                self.file_times.insert(file, modified);
184
24
            
}0
185
0
        }
186
24
    }
187
}
188
189
/// Incremental scoring engine with caching
190
pub struct ScoreEngine {
191
    config: ScoreConfig,
192
    cache: HashMap<CacheKey, CacheEntry>,
193
    dependency_tracker: DependencyTracker,
194
}
195
196
impl ScoreEngine {
197
24
    pub fn new(config: ScoreConfig) -> Self {
198
24
        Self { 
199
24
            config,
200
24
            cache: HashMap::new(),
201
24
            dependency_tracker: DependencyTracker::new(),
202
24
        }
203
24
    }
204
    
205
0
    pub fn score(&self, ast: &crate::frontend::ast::Expr, depth: AnalysisDepth) -> QualityScore {
206
0
        let components = match depth {
207
0
            AnalysisDepth::Shallow => Self::score_shallow(ast),
208
0
            AnalysisDepth::Standard => Self::score_standard(ast),
209
0
            AnalysisDepth::Deep => Self::score_deep(ast),
210
        };
211
        
212
0
        let value = self.calculate_weighted_score(&components);
213
0
        let grade = Grade::from_score(value);
214
0
        let confidence = Self::calculate_confidence(depth);
215
        
216
0
        QualityScore {
217
0
            value,
218
0
            components,
219
0
            grade,
220
0
            confidence,
221
0
            cache_hit_rate: 0.0, // No file path in legacy API
222
0
        }
223
0
    }
224
225
    /// Incremental scoring with file-based caching (RUCHY-0813)
226
24
    pub fn score_incremental(
227
24
        &mut self,
228
24
        ast: &crate::frontend::ast::Expr,
229
24
        file_path: PathBuf,
230
24
        content: &str,
231
24
        depth: AnalysisDepth,
232
24
    ) -> QualityScore {
233
24
        let content_hash = Self::hash_content(content);
234
24
        let cache_key = CacheKey {
235
24
            file_path: file_path.clone(),
236
24
            content_hash,
237
24
            depth,
238
24
        };
239
240
        // Check cache first
241
24
        if let Some(
entry0
) = self.cache.get(&cache_key) {
242
0
            if !self.dependency_tracker.is_stale(&file_path) {
243
0
                let mut score = entry.score.clone();
244
0
                score.cache_hit_rate = 1.0;
245
0
                return score;
246
0
            }
247
24
        }
248
249
        // Fast path for small files - skip complex analysis
250
24
        let start = std::time::Instant::now();
251
24
        let is_small_file = content.len() < 1024;
252
24
        let effective_depth = if is_small_file && depth != AnalysisDepth::Deep {
253
23
            AnalysisDepth::Shallow
254
        } else {
255
1
            depth
256
        };
257
258
24
        let components = match effective_depth {
259
23
            AnalysisDepth::Shallow => Self::score_shallow(ast),
260
0
            AnalysisDepth::Standard => Self::score_standard(ast),
261
1
            AnalysisDepth::Deep => Self::score_deep(ast),
262
        };
263
264
24
        let value = self.calculate_weighted_score(&components);
265
24
        let grade = Grade::from_score(value);
266
24
        let confidence = if is_small_file && depth != effective_depth {
267
22
            Self::calculate_confidence(effective_depth) * 0.9 // Slightly reduced confidence for fast path
268
        } else {
269
2
            Self::calculate_confidence(depth)
270
        };
271
24
        let elapsed = start.elapsed();
272
273
24
        let score = QualityScore {
274
24
            value,
275
24
            components,
276
24
            grade,
277
24
            confidence,
278
24
            cache_hit_rate: 0.0,
279
24
        };
280
281
        // Cache the result only if worth caching
282
24
        let is_worth_caching = elapsed > Duration::from_millis(10) || !is_small_file;
283
24
        if is_worth_caching {
284
0
            let entry = CacheEntry {
285
0
                score: score.clone(),
286
0
                timestamp: SystemTime::now(),
287
0
                dependencies: Self::extract_dependencies(ast),
288
0
            };
289
0
            self.cache.insert(cache_key, entry);
290
24
        }
291
292
24
        self.dependency_tracker.update_file_time(file_path);
293
294
        // Maintain cache if scoring took too long or cache is getting large
295
24
        if elapsed > Duration::from_millis(100) || self.cache.len() > 1000 {
296
0
            self.optimize_cache();
297
24
        }
298
299
24
        score
300
24
    }
301
302
    /// Progressive scoring that refines analysis depth based on time budget
303
0
    pub fn score_progressive(
304
0
        &mut self,
305
0
        ast: &crate::frontend::ast::Expr,
306
0
        file_path: PathBuf,
307
0
        content: &str,
308
0
        time_budget: Duration,
309
0
    ) -> QualityScore {
310
0
        let start = std::time::Instant::now();
311
312
        // Start with shallow analysis
313
0
        let mut score = self.score_incremental(ast, file_path.clone(), content, AnalysisDepth::Shallow);
314
        
315
0
        if start.elapsed() < time_budget / 3 {
316
            // Upgrade to standard analysis
317
0
            score = self.score_incremental(ast, file_path.clone(), content, AnalysisDepth::Standard);
318
            
319
0
            if start.elapsed() < time_budget * 2 / 3 {
320
0
                // Upgrade to deep analysis
321
0
                score = self.score_incremental(ast, file_path, content, AnalysisDepth::Deep);
322
0
            }
323
0
        }
324
325
0
        score
326
0
    }
327
328
24
    fn hash_content(content: &str) -> u64 {
329
        use std::collections::hash_map::DefaultHasher;
330
        use std::hash::{Hash, Hasher};
331
        
332
24
        let mut hasher = DefaultHasher::new();
333
24
        content.hash(&mut hasher);
334
24
        hasher.finish()
335
24
    }
336
337
0
    fn extract_dependencies(_ast: &crate::frontend::ast::Expr) -> Vec<PathBuf> {
338
        // Extract import/use dependencies from AST
339
        // Implementation in RUCHY-0814 with type checker integration
340
0
        Vec::new()
341
0
    }
342
343
0
    fn optimize_cache(&mut self) {
344
        // Remove old cache entries to maintain <100ms performance
345
0
        let now = SystemTime::now();
346
0
        let cutoff = Duration::from_secs(300); // 5 minutes
347
0
        let max_entries = 500; // Maximum cache entries for performance
348
349
        // First pass: Remove old entries
350
0
        self.cache.retain(|_, entry| {
351
0
            if let Ok(age) = now.duration_since(entry.timestamp) {
352
0
                age < cutoff
353
            } else {
354
0
                false
355
            }
356
0
        });
357
358
        // Second pass: If still too many entries, remove least recently used
359
0
        if self.cache.len() > max_entries {
360
0
            let mut entries: Vec<_> = self.cache.iter().map(|(k, v)| (k.clone(), v.timestamp)).collect();
361
0
            entries.sort_by_key(|(_, timestamp)| *timestamp);
362
            
363
0
            let to_remove = self.cache.len() - max_entries;
364
0
            let keys_to_remove: Vec<_> = entries.iter()
365
0
                .take(to_remove)
366
0
                .map(|(k, _)| k.clone())
367
0
                .collect();
368
            
369
0
            for key in keys_to_remove {
370
0
                self.cache.remove(&key);
371
0
            }
372
0
        }
373
0
    }
374
375
    /// Clear all caches - useful for memory management
376
0
    pub fn clear_cache(&mut self) {
377
0
        self.cache.clear();
378
0
        self.dependency_tracker = DependencyTracker::new();
379
0
    }
380
381
    /// Get cache statistics
382
0
    pub fn cache_stats(&self) -> CacheStats {
383
0
        CacheStats {
384
0
            entries: self.cache.len(),
385
0
            memory_usage_estimate: self.cache.len() * 1024, // Rough estimate
386
0
        }
387
0
    }
388
    
389
24
    fn score_shallow(ast: &crate::frontend::ast::Expr) -> ScoreComponents {
390
        // Fast AST-only analysis (<100ms)
391
24
        let metrics = analyze_ast_metrics(ast);
392
        
393
24
        let correctness = 1.0;
394
24
        let mut performance = 1.0;
395
24
        let mut maintainability = 1.0;
396
24
        let safety = 1.0;
397
24
        let idiomaticity = 1.0;
398
        
399
        // Penalize high complexity
400
24
        if metrics.max_depth > 10 {
401
0
            maintainability *= 0.9;
402
24
        }
403
24
        if metrics.function_count > 50 {
404
0
            maintainability *= 0.95;
405
24
        }
406
        
407
        // Penalize deep nesting
408
24
        if metrics.max_nesting > 5 {
409
0
            performance *= 0.9;
410
0
            maintainability *= 0.9;
411
24
        }
412
        
413
        // Penalize excessive lines
414
24
        if metrics.line_count > 1000 {
415
0
            maintainability *= 0.95;
416
24
        }
417
        
418
24
        ScoreComponents {
419
24
            correctness,
420
24
            performance,
421
24
            maintainability,
422
24
            safety,
423
24
            idiomaticity,
424
24
        }
425
24
    }
426
    
427
1
    fn score_standard(ast: &crate::frontend::ast::Expr) -> ScoreComponents {
428
        // Standard analysis with type checking (<1s)
429
1
        let mut components = Self::score_shallow(ast);
430
        
431
        // Additional type-based analysis
432
        // Type checker integration in RUCHY-0814
433
1
        components.correctness *= 0.95;
434
1
        components.safety *= 0.95;
435
        
436
1
        components
437
1
    }
438
    
439
1
    fn score_deep(ast: &crate::frontend::ast::Expr) -> ScoreComponents {
440
        // Deep analysis with property testing (<30s)
441
1
        let mut components = Self::score_standard(ast);
442
        
443
        // Additional deep analysis
444
        // Property testing and mutation testing in RUCHY-0816
445
1
        components.correctness *= 0.98;
446
        
447
1
        components
448
1
    }
449
    
450
24
    fn calculate_weighted_score(&self, components: &ScoreComponents) -> f64 {
451
24
        components.correctness * self.config.correctness_weight
452
24
            + components.performance * self.config.performance_weight
453
24
            + components.maintainability * self.config.maintainability_weight
454
24
            + components.safety * self.config.safety_weight
455
24
            + components.idiomaticity * self.config.idiomaticity_weight
456
24
    }
457
    
458
24
    fn calculate_confidence(depth: AnalysisDepth) -> f64 {
459
24
        match depth {
460
23
            AnalysisDepth::Shallow => 0.6,
461
0
            AnalysisDepth::Standard => 0.8,
462
1
            AnalysisDepth::Deep => 0.95,
463
        }
464
24
    }
465
}
466
467
/// AST metrics for analysis
468
#[derive(Debug)]
469
struct AstMetrics {
470
    function_count: usize,
471
    max_depth: usize,
472
    max_nesting: usize,
473
    line_count: usize,
474
    cyclomatic_complexity: usize,
475
}
476
477
24
fn analyze_ast_metrics(ast: &crate::frontend::ast::Expr) -> AstMetrics {
478
24
    let mut metrics = AstMetrics {
479
24
        function_count: 0,
480
24
        max_depth: 0,
481
24
        max_nesting: 0,
482
24
        line_count: 0,
483
24
        cyclomatic_complexity: 1, // Base complexity
484
24
    };
485
    
486
24
    analyze_expr(ast, &mut metrics, 0, 0);
487
24
    metrics
488
24
}
489
490
30
fn analyze_expr(expr: &crate::frontend::ast::Expr, metrics: &mut AstMetrics, depth: usize, nesting: usize) {
491
    
492
30
    metrics.max_depth = metrics.max_depth.max(depth);
493
30
    metrics.max_nesting = metrics.max_nesting.max(nesting);
494
    
495
30
    match &expr.kind {
496
0
        ExprKind::Function { body, .. } => {
497
0
            metrics.function_count += 1;
498
0
            analyze_expr(body, metrics, depth + 1, 0);
499
0
        }
500
3
        ExprKind::Block(exprs) => {
501
9
            for 
e6
in exprs {
502
6
                analyze_expr(e, metrics, depth + 1, nesting);
503
6
            }
504
        }
505
0
        ExprKind::If { condition, then_branch, else_branch } => {
506
0
            metrics.cyclomatic_complexity += 1;
507
0
            analyze_expr(condition, metrics, depth + 1, nesting + 1);
508
0
            analyze_expr(then_branch, metrics, depth + 1, nesting + 1);
509
0
            if let Some(else_expr) = else_branch {
510
0
                analyze_expr(else_expr, metrics, depth + 1, nesting + 1);
511
0
            }
512
        }
513
0
        ExprKind::While { condition, body } => {
514
0
            metrics.cyclomatic_complexity += 1;
515
0
            analyze_expr(condition, metrics, depth + 1, nesting + 1);
516
0
            analyze_expr(body, metrics, depth + 1, nesting + 1);
517
0
        }
518
0
        ExprKind::For { iter, body, .. } => {
519
0
            metrics.cyclomatic_complexity += 1;
520
0
            analyze_expr(iter, metrics, depth + 1, nesting + 1);
521
0
            analyze_expr(body, metrics, depth + 1, nesting + 1);
522
0
        }
523
0
        ExprKind::Match { expr: match_expr, arms } => {
524
0
            analyze_expr(match_expr, metrics, depth + 1, nesting);
525
0
            for arm in arms {
526
0
                metrics.cyclomatic_complexity += 1;
527
0
                // Guards have been removed from the grammar
528
0
                analyze_expr(&arm.body, metrics, depth + 1, nesting + 1);
529
0
            }
530
        }
531
27
        _ => {}
532
    }
533
30
}
534
535
impl QualityScore {
536
    /// Explain changes from a baseline score
537
0
    pub fn explain_delta(&self, baseline: &QualityScore) -> ScoreExplanation {
538
0
        let delta = self.value - baseline.value;
539
0
        let mut changes = Vec::new();
540
0
        let mut tradeoffs = Vec::new();
541
        
542
        // Track component changes
543
0
        let components = [
544
0
            ("Correctness", self.components.correctness, baseline.components.correctness),
545
0
            ("Performance", self.components.performance, baseline.components.performance),
546
0
            ("Maintainability", self.components.maintainability, baseline.components.maintainability),
547
0
            ("Safety", self.components.safety, baseline.components.safety),
548
0
            ("Idiomaticity", self.components.idiomaticity, baseline.components.idiomaticity),
549
0
        ];
550
        
551
0
        for (name, current, baseline) in components {
552
0
            let diff = current - baseline;
553
0
            if diff.abs() > 0.01 {
554
0
                changes.push(format!("{}: {}{:.1}%", 
555
                    name,
556
0
                    if diff > 0.0 { "+" } else { "" },
557
0
                    diff * 100.0
558
                ));
559
0
            }
560
        }
561
        
562
        // Detect tradeoffs
563
0
        if self.components.performance > baseline.components.performance 
564
0
            && self.components.maintainability < baseline.components.maintainability {
565
0
            tradeoffs.push("Performance improved at the cost of maintainability".to_string());
566
0
        }
567
        
568
0
        if self.components.safety > baseline.components.safety 
569
0
            && self.components.performance < baseline.components.performance {
570
0
            tradeoffs.push("Safety improved at the cost of performance".to_string());
571
0
        }
572
        
573
0
        ScoreExplanation {
574
0
            delta,
575
0
            changes,
576
0
            tradeoffs,
577
0
            grade_change: format!("{} → {}", baseline.grade, self.grade),
578
0
        }
579
0
    }
580
}
581
582
/// Explanation of score changes
583
pub struct ScoreExplanation {
584
    pub delta: f64,
585
    pub changes: Vec<String>,
586
    pub tradeoffs: Vec<String>,
587
    pub grade_change: String,
588
}
589
590
/// Cache performance statistics
591
#[derive(Debug, Clone)]
592
pub struct CacheStats {
593
    pub entries: usize,
594
    pub memory_usage_estimate: usize,
595
}
596
597
/// Score correctness component (35% weight)
598
0
pub fn score_correctness(ast: &crate::frontend::ast::Expr) -> f64 {
599
0
    let mut score = 1.0;
600
    
601
    // Pattern match exhaustiveness check
602
0
    let pattern_completeness = analyze_pattern_completeness(ast);
603
0
    score *= pattern_completeness;
604
    
605
    // Error handling coverage
606
0
    let error_handling_quality = analyze_error_handling(ast);
607
0
    score *= error_handling_quality;
608
    
609
    // Type consistency analysis
610
0
    let type_consistency = analyze_type_consistency(ast);
611
0
    score *= type_consistency;
612
    
613
    // Logical soundness (basic checks)
614
0
    let logical_soundness = analyze_logical_soundness(ast);
615
0
    score *= logical_soundness;
616
    
617
0
    score.clamp(0.0, 1.0)
618
0
}
619
620
/// Analyze pattern match completeness
621
0
fn analyze_pattern_completeness(ast: &crate::frontend::ast::Expr) -> f64 {
622
0
    let mut total_matches = 0;
623
0
    let mut complete_matches = 0;
624
    
625
0
    analyze_pattern_completeness_recursive(ast, &mut total_matches, &mut complete_matches);
626
    
627
0
    if total_matches == 0 {
628
0
        1.0 // No matches, assume complete
629
    } else {
630
        #[allow(clippy::cast_precision_loss)]
631
0
        let score = (complete_matches as f64) / (total_matches as f64);
632
0
        score
633
    }
634
0
}
635
636
0
fn analyze_pattern_completeness_recursive(
637
0
    expr: &crate::frontend::ast::Expr,
638
0
    total_matches: &mut usize,
639
0
    complete_matches: &mut usize,
640
0
) {
641
    
642
0
    match &expr.kind {
643
0
        ExprKind::Match { expr: match_expr, arms } => {
644
0
            *total_matches += 1;
645
            
646
            // Check if match has wildcard pattern or covers all cases
647
0
            let has_wildcard = arms.iter().any(|arm| {
648
0
                matches!(arm.pattern, crate::frontend::ast::Pattern::Wildcard)
649
0
            });
650
            
651
0
            if has_wildcard || arms.len() >= 2 { // Basic heuristic
652
0
                *complete_matches += 1;
653
0
            }
654
            
655
0
            analyze_pattern_completeness_recursive(match_expr, total_matches, complete_matches);
656
0
            for arm in arms {
657
0
                analyze_pattern_completeness_recursive(&arm.body, total_matches, complete_matches);
658
0
            }
659
        }
660
0
        ExprKind::If { condition, then_branch, else_branch } => {
661
0
            analyze_pattern_completeness_recursive(condition, total_matches, complete_matches);
662
0
            analyze_pattern_completeness_recursive(then_branch, total_matches, complete_matches);
663
0
            if let Some(else_expr) = else_branch {
664
0
                analyze_pattern_completeness_recursive(else_expr, total_matches, complete_matches);
665
0
            }
666
        }
667
0
        ExprKind::Block(exprs) => {
668
0
            for e in exprs {
669
0
                analyze_pattern_completeness_recursive(e, total_matches, complete_matches);
670
0
            }
671
        }
672
0
        ExprKind::Function { body, .. } => {
673
0
            analyze_pattern_completeness_recursive(body, total_matches, complete_matches);
674
0
        }
675
0
        _ => {}
676
    }
677
0
}
678
679
/// Analyze error handling quality
680
0
fn analyze_error_handling(ast: &crate::frontend::ast::Expr) -> f64 {
681
0
    let mut total_fallible_ops = 0;
682
0
    let mut handled_ops = 0;
683
    
684
0
    analyze_error_handling_recursive(ast, &mut total_fallible_ops, &mut handled_ops);
685
    
686
0
    if total_fallible_ops == 0 {
687
0
        1.0 // No fallible operations
688
    } else {
689
        #[allow(clippy::cast_precision_loss)]
690
0
        let base_score = (handled_ops as f64) / (total_fallible_ops as f64);
691
        // Boost score if some error handling is present
692
0
        if handled_ops > 0 {
693
0
            (base_score + 0.3).min(1.0)
694
        } else {
695
0
            0.7 // Penalty for no error handling
696
        }
697
    }
698
0
}
699
700
0
fn analyze_error_handling_recursive(
701
0
    expr: &crate::frontend::ast::Expr,
702
0
    total_fallible_ops: &mut usize,
703
0
    handled_ops: &mut usize,
704
0
) {
705
    
706
0
    match &expr.kind {
707
0
        ExprKind::Match { expr: match_expr, arms } => {
708
            // Check if matching on Result type (heuristic)
709
0
            if arms.len() >= 2 {
710
0
                *total_fallible_ops += 1;
711
0
                *handled_ops += 1;
712
0
            }
713
0
            analyze_error_handling_recursive(match_expr, total_fallible_ops, handled_ops);
714
0
            for arm in arms {
715
0
                analyze_error_handling_recursive(&arm.body, total_fallible_ops, handled_ops);
716
0
            }
717
        }
718
0
        ExprKind::Block(exprs) => {
719
0
            for e in exprs {
720
0
                analyze_error_handling_recursive(e, total_fallible_ops, handled_ops);
721
0
            }
722
        }
723
0
        ExprKind::Function { body, .. } => {
724
0
            analyze_error_handling_recursive(body, total_fallible_ops, handled_ops);
725
0
        }
726
0
        _ => {}
727
    }
728
0
}
729
730
/// Analyze type consistency
731
0
fn analyze_type_consistency(_ast: &crate::frontend::ast::Expr) -> f64 {
732
    // For now, assume good consistency since we have type checking
733
    // Future: integrate with type checker for real analysis
734
0
    0.95
735
0
}
736
737
/// Analyze logical soundness
738
0
fn analyze_logical_soundness(ast: &crate::frontend::ast::Expr) -> f64 {
739
0
    let mut score = 1.0;
740
    
741
    // Check for obvious logical issues
742
0
    let has_unreachable = has_unreachable_code(ast);
743
0
    if has_unreachable {
744
0
        score *= 0.8; // Penalty for unreachable code
745
0
    }
746
    
747
0
    let has_infinite_loops = has_potential_infinite_loops(ast);
748
0
    if has_infinite_loops {
749
0
        score *= 0.9; // Penalty for potential infinite loops
750
0
    }
751
    
752
0
    score
753
0
}
754
755
0
fn has_unreachable_code(ast: &crate::frontend::ast::Expr) -> bool {
756
    
757
0
    match &ast.kind {
758
0
        ExprKind::Block(exprs) => {
759
0
            for (i, expr) in exprs.iter().enumerate() {
760
0
                if i < exprs.len() - 1 && is_diverging_expr(expr) {
761
0
                    return true; // Code after diverging expression
762
0
                }
763
0
                if has_unreachable_code(expr) {
764
0
                    return true;
765
0
                }
766
            }
767
0
            false
768
        }
769
0
        ExprKind::If { condition, then_branch, else_branch } => {
770
0
            has_unreachable_code(condition) ||
771
0
            has_unreachable_code(then_branch) ||
772
0
            else_branch.as_ref().is_some_and(|e| has_unreachable_code(e))
773
        }
774
0
        _ => false
775
    }
776
0
}
777
778
0
fn is_diverging_expr(expr: &crate::frontend::ast::Expr) -> bool {
779
    
780
0
    match &expr.kind {
781
0
        ExprKind::Call { func, .. } => {
782
            // Check for known diverging functions (heuristic)
783
0
            if let ExprKind::Identifier(name) = &func.kind {
784
0
                matches!(name.as_str(), "panic" | "unreachable" | "exit")
785
            } else {
786
0
                false
787
            }
788
        }
789
0
        _ => false
790
    }
791
0
}
792
793
0
fn has_potential_infinite_loops(ast: &crate::frontend::ast::Expr) -> bool {
794
    
795
0
    match &ast.kind {
796
0
        ExprKind::While { condition, body } => {
797
            // Check for trivial infinite loops: while true { ... }
798
0
            if let ExprKind::Literal(crate::frontend::ast::Literal::Bool(true)) = &condition.kind {
799
                // Check if body has break statement
800
0
                !has_break_statement(body)
801
            } else {
802
0
                has_potential_infinite_loops(condition) || has_potential_infinite_loops(body)
803
            }
804
        }
805
0
        ExprKind::Block(exprs) => exprs.iter().any(has_potential_infinite_loops),
806
0
        ExprKind::Function { body, .. } => has_potential_infinite_loops(body),
807
0
        _ => false
808
    }
809
0
}
810
811
0
fn has_break_statement(ast: &crate::frontend::ast::Expr) -> bool {
812
    
813
0
    match &ast.kind {
814
0
        ExprKind::Break { .. } => true,
815
0
        ExprKind::Block(exprs) => exprs.iter().any(has_break_statement),
816
0
        ExprKind::If { condition, then_branch, else_branch } => {
817
0
            has_break_statement(condition) ||
818
0
            has_break_statement(then_branch) ||
819
0
            else_branch.as_ref().is_some_and(|e| has_break_statement(e))
820
        }
821
0
        _ => false
822
    }
823
0
}
824
825
/// Score performance component (25% weight)
826
0
pub fn score_performance(ast: &crate::frontend::ast::Expr) -> f64 {
827
0
    let metrics = analyze_ast_metrics(ast);
828
0
    let mut score = 1.0;
829
    
830
    // Complexity analysis (BigO implications)
831
0
    let complexity_score = analyze_algorithmic_complexity(ast);
832
0
    score *= complexity_score;
833
    
834
    // Penalize high cyclomatic complexity (affects branch prediction)
835
0
    if metrics.cyclomatic_complexity > 10 {
836
0
        #[allow(clippy::cast_precision_loss)]
837
0
        let penalty = ((metrics.cyclomatic_complexity - 10) as f64 * 0.02).min(0.3);
838
0
        score *= 1.0 - penalty;
839
0
    }
840
    
841
    // Penalize deep nesting (affects cache performance)
842
0
    if metrics.max_nesting > 3 {
843
0
        #[allow(clippy::cast_precision_loss)]
844
0
        let penalty = ((metrics.max_nesting - 3) as f64 * 0.05).min(0.2);
845
0
        score *= 1.0 - penalty;
846
0
    }
847
    
848
    // Allocation analysis
849
0
    let allocation_score = analyze_allocation_patterns(ast);
850
0
    score *= allocation_score;
851
    
852
    // Memory access patterns (simplified for current AST)
853
    // Future enhancement when Index/Dict variants are added
854
    
855
0
    score.clamp(0.0, 1.0)
856
0
}
857
858
/// Analyze algorithmic complexity patterns
859
0
fn analyze_algorithmic_complexity(ast: &crate::frontend::ast::Expr) -> f64 {
860
0
    let mut nested_loops = 0;
861
0
    let mut recursive_calls = 0;
862
    
863
0
    analyze_complexity_recursive(ast, &mut nested_loops, &mut recursive_calls, 0);
864
    
865
0
    let mut score = 1.0;
866
    
867
    // Penalize nested loops (O(n^k) complexity)
868
0
    if nested_loops > 0 {
869
0
        #[allow(clippy::cast_precision_loss)]
870
0
        let penalty = (f64::from(nested_loops) * 0.15).min(0.5);
871
0
        score *= 1.0 - penalty;
872
0
    }
873
    
874
    // Penalize recursive calls without obvious base case
875
0
    if recursive_calls > 2 {
876
0
        score *= 0.8; // May indicate exponential complexity
877
0
    }
878
    
879
0
    score
880
0
}
881
882
0
fn analyze_complexity_recursive(
883
0
    expr: &crate::frontend::ast::Expr,
884
0
    nested_loops: &mut i32,
885
0
    recursive_calls: &mut i32,
886
0
    current_nesting: i32,
887
0
) {
888
    
889
0
    match &expr.kind {
890
0
        ExprKind::For { iter, body, .. } | ExprKind::While { condition: iter, body } => {
891
0
            if current_nesting > 0 {
892
0
                *nested_loops += 1;
893
0
            }
894
0
            analyze_complexity_recursive(iter, nested_loops, recursive_calls, current_nesting);
895
0
            analyze_complexity_recursive(body, nested_loops, recursive_calls, current_nesting + 1);
896
        }
897
0
        ExprKind::Call { func, args } => {
898
            // Check for potential recursive calls (heuristic)
899
0
            if let ExprKind::Identifier(_) = &func.kind {
900
0
                *recursive_calls += 1;
901
0
            }
902
0
            analyze_complexity_recursive(func, nested_loops, recursive_calls, current_nesting);
903
0
            for arg in args {
904
0
                analyze_complexity_recursive(arg, nested_loops, recursive_calls, current_nesting);
905
0
            }
906
        }
907
0
        ExprKind::Block(exprs) => {
908
0
            for e in exprs {
909
0
                analyze_complexity_recursive(e, nested_loops, recursive_calls, current_nesting);
910
0
            }
911
        }
912
0
        ExprKind::Function { body, .. } => {
913
0
            analyze_complexity_recursive(body, nested_loops, recursive_calls, 0);
914
0
        }
915
0
        ExprKind::If { condition, then_branch, else_branch } => {
916
0
            analyze_complexity_recursive(condition, nested_loops, recursive_calls, current_nesting);
917
0
            analyze_complexity_recursive(then_branch, nested_loops, recursive_calls, current_nesting);
918
0
            if let Some(else_expr) = else_branch {
919
0
                analyze_complexity_recursive(else_expr, nested_loops, recursive_calls, current_nesting);
920
0
            }
921
        }
922
0
        _ => {}
923
    }
924
0
}
925
926
/// Analyze allocation patterns (GC pressure)
927
0
fn analyze_allocation_patterns(ast: &crate::frontend::ast::Expr) -> f64 {
928
0
    let mut allocations = 0;
929
0
    let mut large_allocations = 0;
930
    
931
0
    count_allocations_recursive(ast, &mut allocations, &mut large_allocations);
932
    
933
0
    let mut score = 1.0;
934
    
935
    // Penalize excessive allocations
936
0
    if allocations > 10 {
937
0
        #[allow(clippy::cast_precision_loss)]
938
0
        let penalty = (f64::from(allocations - 10) * 0.01).min(0.3);
939
0
        score *= 1.0 - penalty;
940
0
    }
941
    
942
    // Penalize large allocations in loops
943
0
    if large_allocations > 0 {
944
0
        #[allow(clippy::cast_precision_loss)]
945
0
        let penalty = (f64::from(large_allocations) * 0.1).min(0.4);
946
0
        score *= 1.0 - penalty;
947
0
    }
948
    
949
0
    score
950
0
}
951
952
0
fn count_allocations_recursive(
953
0
    expr: &crate::frontend::ast::Expr,
954
0
    allocations: &mut i32,
955
0
    large_allocations: &mut i32,
956
0
) {
957
    
958
0
    match &expr.kind {
959
0
        ExprKind::List(items) => {
960
0
            *allocations += 1;
961
0
            if items.len() > 100 {
962
0
                *large_allocations += 1;
963
0
            }
964
0
            for item in items {
965
0
                count_allocations_recursive(item, allocations, large_allocations);
966
0
            }
967
        }
968
        // Dictionary literals not yet implemented in AST
969
        // ExprKind::Dict { pairs } => { ... }
970
0
        ExprKind::StringInterpolation { parts } => {
971
0
            *allocations += 1; // String concatenation
972
0
            for part in parts {
973
0
                if let crate::frontend::ast::StringPart::Expr(e) = part {
974
0
                    count_allocations_recursive(e, allocations, large_allocations);
975
0
                }
976
            }
977
        }
978
0
        ExprKind::Block(exprs) => {
979
0
            for e in exprs {
980
0
                count_allocations_recursive(e, allocations, large_allocations);
981
0
            }
982
        }
983
0
        ExprKind::Function { body, .. } => {
984
0
            count_allocations_recursive(body, allocations, large_allocations);
985
0
        }
986
0
        _ => {}
987
    }
988
0
}
989
990
// Memory access pattern analysis will be added when
991
// AST supports indexing and dictionary operations
992
993
/// Score maintainability component (20% weight)
994
0
pub fn score_maintainability(ast: &crate::frontend::ast::Expr) -> f64 {
995
0
    let metrics = analyze_ast_metrics(ast);
996
0
    let mut score = 1.0;
997
    
998
    // Coupling analysis
999
0
    let coupling_score = analyze_coupling(ast);
1000
0
    score *= coupling_score;
1001
    
1002
    // Cohesion analysis
1003
0
    let cohesion_score = analyze_cohesion(ast, &metrics);
1004
0
    score *= cohesion_score;
1005
    
1006
    // Code duplication detection (simplified)
1007
0
    let duplication_score = analyze_duplication(ast);
1008
0
    score *= duplication_score;
1009
    
1010
    // Naming quality (basic heuristics)
1011
0
    let naming_score = analyze_naming_quality(ast);
1012
0
    score *= naming_score;
1013
    
1014
0
    score.clamp(0.0, 1.0)
1015
0
}
1016
1017
/// Analyze coupling between functions/modules
1018
0
fn analyze_coupling(ast: &crate::frontend::ast::Expr) -> f64 {
1019
0
    let mut external_calls = 0;
1020
0
    let mut total_functions = 0;
1021
    
1022
0
    count_coupling_metrics(ast, &mut external_calls, &mut total_functions);
1023
    
1024
0
    if total_functions == 0 {
1025
0
        return 1.0;
1026
0
    }
1027
    
1028
    #[allow(clippy::cast_precision_loss)]
1029
0
    let coupling_ratio = f64::from(external_calls) / f64::from(total_functions);
1030
    
1031
    // Lower coupling is better
1032
0
    if coupling_ratio > 5.0 {
1033
0
        0.7 // High coupling penalty
1034
0
    } else if coupling_ratio > 2.0 {
1035
0
        0.85
1036
    } else {
1037
0
        1.0 // Good coupling
1038
    }
1039
0
}
1040
1041
0
fn count_coupling_metrics(expr: &crate::frontend::ast::Expr, external_calls: &mut i32, total_functions: &mut i32) {
1042
    
1043
0
    match &expr.kind {
1044
0
        ExprKind::Function { body, .. } => {
1045
0
            *total_functions += 1;
1046
0
            count_coupling_metrics(body, external_calls, total_functions);
1047
0
        }
1048
0
        ExprKind::Call { func, args } => {
1049
0
            *external_calls += 1;
1050
0
            count_coupling_metrics(func, external_calls, total_functions);
1051
0
            for arg in args {
1052
0
                count_coupling_metrics(arg, external_calls, total_functions);
1053
0
            }
1054
        }
1055
0
        ExprKind::Block(exprs) => {
1056
0
            for e in exprs {
1057
0
                count_coupling_metrics(e, external_calls, total_functions);
1058
0
            }
1059
        }
1060
0
        _ => {}
1061
    }
1062
0
}
1063
1064
/// Analyze cohesion within functions
1065
0
fn analyze_cohesion(_ast: &crate::frontend::ast::Expr, metrics: &AstMetrics) -> f64 {
1066
0
    let mut score = 1.0;
1067
    
1068
    // Penalize functions that are too large (low cohesion indicator)
1069
0
    if metrics.line_count > 100 {
1070
0
        score *= 0.8;
1071
0
    }
1072
    
1073
    // Penalize excessive depth (indicates mixed concerns)
1074
0
    if metrics.max_depth > 15 {
1075
0
        score *= 0.85;
1076
0
    }
1077
    
1078
    // Penalize too many functions (possible low cohesion)
1079
0
    if metrics.function_count > 30 {
1080
0
        score *= 0.9;
1081
0
    }
1082
    
1083
0
    score
1084
0
}
1085
1086
/// Analyze code duplication (simplified heuristic)
1087
0
fn analyze_duplication(ast: &crate::frontend::ast::Expr) -> f64 {
1088
0
    let mut expression_patterns = std::collections::HashMap::new();
1089
    
1090
0
    collect_expression_patterns(ast, &mut expression_patterns);
1091
    
1092
0
    let duplicated_patterns = expression_patterns.values().filter(|&&count| count > 1).count();
1093
    
1094
0
    if duplicated_patterns > 5 {
1095
0
        0.8 // Significant duplication penalty
1096
0
    } else if duplicated_patterns > 2 {
1097
0
        0.9 // Some duplication
1098
    } else {
1099
0
        1.0 // Little to no duplication
1100
    }
1101
0
}
1102
1103
0
fn collect_expression_patterns(expr: &crate::frontend::ast::Expr, patterns: &mut std::collections::HashMap<String, i32>) {
1104
    
1105
    // Simplified pattern matching - use expression kind as pattern
1106
0
    let pattern = format!("{:?}", std::mem::discriminant(&expr.kind));
1107
0
    *patterns.entry(pattern).or_insert(0) += 1;
1108
    
1109
0
    match &expr.kind {
1110
0
        ExprKind::Block(exprs) => {
1111
0
            for e in exprs {
1112
0
                collect_expression_patterns(e, patterns);
1113
0
            }
1114
        }
1115
0
        ExprKind::Function { body, .. } => {
1116
0
            collect_expression_patterns(body, patterns);
1117
0
        }
1118
0
        ExprKind::If { condition, then_branch, else_branch } => {
1119
0
            collect_expression_patterns(condition, patterns);
1120
0
            collect_expression_patterns(then_branch, patterns);
1121
0
            if let Some(else_expr) = else_branch {
1122
0
                collect_expression_patterns(else_expr, patterns);
1123
0
            }
1124
        }
1125
0
        _ => {}
1126
    }
1127
0
}
1128
1129
/// Analyze naming quality (basic heuristics)
1130
0
fn analyze_naming_quality(ast: &crate::frontend::ast::Expr) -> f64 {
1131
0
    let mut good_names = 0;
1132
0
    let mut total_names = 0;
1133
    
1134
0
    analyze_names_recursive(ast, &mut good_names, &mut total_names);
1135
    
1136
0
    if total_names == 0 {
1137
0
        return 1.0;
1138
0
    }
1139
    
1140
    #[allow(clippy::cast_precision_loss)]
1141
0
    let good_ratio = f64::from(good_names) / f64::from(total_names);
1142
0
    good_ratio.max(0.5) // Minimum score for naming
1143
0
}
1144
1145
0
fn analyze_names_recursive(expr: &crate::frontend::ast::Expr, good_names: &mut i32, total_names: &mut i32) {
1146
    
1147
0
    match &expr.kind {
1148
0
        ExprKind::Function { name, .. } => {
1149
0
            *total_names += 1;
1150
0
            if is_good_name(name) {
1151
0
                *good_names += 1;
1152
0
            }
1153
        }
1154
0
        ExprKind::Let { name, body, .. } => {
1155
0
            *total_names += 1;
1156
0
            if is_good_name(name) {
1157
0
                *good_names += 1;
1158
0
            }
1159
0
            analyze_names_recursive(body, good_names, total_names);
1160
        }
1161
0
        ExprKind::Block(exprs) => {
1162
0
            for e in exprs {
1163
0
                analyze_names_recursive(e, good_names, total_names);
1164
0
            }
1165
        }
1166
0
        _ => {}
1167
    }
1168
0
}
1169
1170
0
fn is_good_name(name: &str) -> bool {
1171
    // Basic heuristics for good naming
1172
0
    if name.len() < 2 || name.starts_with('_') {
1173
0
        return false;
1174
0
    }
1175
    
1176
    // Check for descriptive names (not single letters or abbreviations)
1177
0
    name.len() >= 3 && !name.chars().all(|c| c.is_ascii_uppercase())
1178
0
}
1179
1180
/// Score safety component (15% weight)
1181
0
pub fn score_safety(ast: &crate::frontend::ast::Expr) -> f64 {
1182
0
    let mut score = 1.0;
1183
    
1184
    // Error handling coverage (reuse from correctness)
1185
0
    let error_handling_quality = analyze_error_handling(ast);
1186
0
    score *= error_handling_quality;
1187
    
1188
    // Null safety analysis
1189
0
    let null_safety_score = analyze_null_safety(ast);
1190
0
    score *= null_safety_score;
1191
    
1192
    // Resource management analysis
1193
0
    let resource_score = analyze_resource_management(ast);
1194
0
    score *= resource_score;
1195
    
1196
    // Bounds checking (implicit in type system, give good score)
1197
0
    score *= 0.95; // Slight penalty for not having explicit bounds checks
1198
    
1199
0
    score.clamp(0.0, 1.0)
1200
0
}
1201
1202
/// Analyze null safety patterns
1203
0
fn analyze_null_safety(ast: &crate::frontend::ast::Expr) -> f64 {
1204
0
    let mut option_uses = 0;
1205
0
    let mut unsafe_accesses = 0;
1206
    
1207
0
    analyze_null_safety_recursive(ast, &mut option_uses, &mut unsafe_accesses);
1208
    
1209
0
    if option_uses + unsafe_accesses == 0 {
1210
0
        return 1.0; // No nullable types used
1211
0
    }
1212
    
1213
    // Prefer Option types over unsafe accesses
1214
0
    if unsafe_accesses == 0 {
1215
0
        1.0 // All nullable accesses are safe
1216
    } else {
1217
        #[allow(clippy::cast_precision_loss)]
1218
0
        let safety_ratio = f64::from(option_uses) / f64::from(option_uses + unsafe_accesses);
1219
0
        safety_ratio.max(0.5) // Minimum safety score
1220
    }
1221
0
}
1222
1223
0
fn analyze_null_safety_recursive(expr: &crate::frontend::ast::Expr, option_uses: &mut i32, unsafe_accesses: &mut i32) {
1224
    
1225
0
    match &expr.kind {
1226
0
        ExprKind::Some { .. } | ExprKind::None => {
1227
0
            *option_uses += 1;
1228
0
        }
1229
0
        ExprKind::Match { arms, .. } => {
1230
            // Check if matching on Option (heuristic)
1231
0
            if arms.len() >= 2 {
1232
0
                *option_uses += 1;
1233
0
            }
1234
0
            for arm in arms {
1235
0
                analyze_null_safety_recursive(&arm.body, option_uses, unsafe_accesses);
1236
0
            }
1237
        }
1238
0
        ExprKind::Block(exprs) => {
1239
0
            for e in exprs {
1240
0
                analyze_null_safety_recursive(e, option_uses, unsafe_accesses);
1241
0
            }
1242
        }
1243
0
        ExprKind::Function { body, .. } => {
1244
0
            analyze_null_safety_recursive(body, option_uses, unsafe_accesses);
1245
0
        }
1246
0
        _ => {}
1247
    }
1248
0
}
1249
1250
/// Analyze resource management patterns
1251
0
fn analyze_resource_management(ast: &crate::frontend::ast::Expr) -> f64 {
1252
0
    let mut resource_allocations = 0;
1253
0
    let mut proper_cleanup = 0;
1254
    
1255
0
    analyze_resources_recursive(ast, &mut resource_allocations, &mut proper_cleanup);
1256
    
1257
0
    if resource_allocations == 0 {
1258
0
        return 1.0; // No resources to manage
1259
0
    }
1260
    
1261
    #[allow(clippy::cast_precision_loss)]
1262
0
    let cleanup_ratio = f64::from(proper_cleanup) / f64::from(resource_allocations);
1263
0
    cleanup_ratio.max(0.7) // Minimum score for resource management
1264
0
}
1265
1266
0
fn analyze_resources_recursive(expr: &crate::frontend::ast::Expr, allocations: &mut i32, cleanup: &mut i32) {
1267
    
1268
0
    match &expr.kind {
1269
0
        ExprKind::Block(exprs) => {
1270
0
            for e in exprs {
1271
0
                analyze_resources_recursive(e, allocations, cleanup);
1272
0
            }
1273
        }
1274
0
        ExprKind::Function { body, .. } => {
1275
0
            analyze_resources_recursive(body, allocations, cleanup);
1276
0
        }
1277
0
        _ => {}
1278
    }
1279
0
}
1280
1281
/// Score idiomaticity component (5% weight)
1282
0
pub fn score_idiomaticity(ast: &crate::frontend::ast::Expr) -> f64 {
1283
0
    let mut score = 1.0;
1284
    
1285
    // Pattern matching usage (idiomatic in Ruchy)
1286
0
    let pattern_score = analyze_pattern_usage(ast);
1287
0
    score *= pattern_score;
1288
    
1289
    // Iterator usage (functional style)
1290
0
    let iterator_score = analyze_iterator_usage(ast);
1291
0
    score *= iterator_score;
1292
    
1293
    // Lambda usage (functional programming)
1294
0
    let lambda_score = analyze_lambda_usage(ast);
1295
0
    score *= lambda_score;
1296
    
1297
0
    score.clamp(0.0, 1.0)
1298
0
}
1299
1300
/// Analyze usage of pattern matching (idiomatic)
1301
0
fn analyze_pattern_usage(ast: &crate::frontend::ast::Expr) -> f64 {
1302
0
    let mut matches = 0;
1303
0
    let mut conditionals = 0;
1304
    
1305
0
    count_pattern_vs_conditional(ast, &mut matches, &mut conditionals);
1306
    
1307
0
    let total = matches + conditionals;
1308
0
    if total == 0 {
1309
0
        return 1.0;
1310
0
    }
1311
    
1312
    #[allow(clippy::cast_precision_loss)]
1313
0
    let pattern_ratio = f64::from(matches) / f64::from(total);
1314
    
1315
    // Higher ratio of matches vs if-else is more idiomatic
1316
0
    if pattern_ratio > 0.7 {
1317
0
        1.0
1318
0
    } else if pattern_ratio > 0.4 {
1319
0
        0.9
1320
    } else {
1321
0
        0.8
1322
    }
1323
0
}
1324
1325
0
fn count_pattern_vs_conditional(expr: &crate::frontend::ast::Expr, matches: &mut i32, conditionals: &mut i32) {
1326
    
1327
0
    match &expr.kind {
1328
0
        ExprKind::Match { arms, .. } => {
1329
0
            *matches += 1;
1330
0
            for arm in arms {
1331
0
                count_pattern_vs_conditional(&arm.body, matches, conditionals);
1332
0
            }
1333
        }
1334
0
        ExprKind::If { condition, then_branch, else_branch } => {
1335
0
            *conditionals += 1;
1336
0
            count_pattern_vs_conditional(condition, matches, conditionals);
1337
0
            count_pattern_vs_conditional(then_branch, matches, conditionals);
1338
0
            if let Some(else_expr) = else_branch {
1339
0
                count_pattern_vs_conditional(else_expr, matches, conditionals);
1340
0
            }
1341
        }
1342
0
        ExprKind::Block(exprs) => {
1343
0
            for e in exprs {
1344
0
                count_pattern_vs_conditional(e, matches, conditionals);
1345
0
            }
1346
        }
1347
0
        ExprKind::Function { body, .. } => {
1348
0
            count_pattern_vs_conditional(body, matches, conditionals);
1349
0
        }
1350
0
        _ => {}
1351
    }
1352
0
}
1353
1354
/// Analyze iterator usage patterns
1355
0
fn analyze_iterator_usage(ast: &crate::frontend::ast::Expr) -> f64 {
1356
0
    let mut iterators = 0;
1357
0
    let mut loops = 0;
1358
    
1359
0
    count_iterator_vs_loops(ast, &mut iterators, &mut loops);
1360
    
1361
0
    let total = iterators + loops;
1362
0
    if total == 0 {
1363
0
        return 1.0;
1364
0
    }
1365
    
1366
    #[allow(clippy::cast_precision_loss)]
1367
0
    let iterator_ratio = f64::from(iterators) / f64::from(total);
1368
    
1369
    // Higher ratio of iterators vs manual loops is more idiomatic
1370
0
    if iterator_ratio > 0.6 {
1371
0
        1.0
1372
0
    } else if iterator_ratio > 0.3 {
1373
0
        0.9
1374
    } else {
1375
0
        0.8
1376
    }
1377
0
}
1378
1379
0
fn count_iterator_vs_loops(expr: &crate::frontend::ast::Expr, iterators: &mut i32, loops: &mut i32) {
1380
    
1381
0
    match &expr.kind {
1382
0
        ExprKind::For { .. } => {
1383
0
            *iterators += 1; // For loops in Ruchy are iterator-based
1384
0
        }
1385
0
        ExprKind::While { .. } => {
1386
0
            *loops += 1;
1387
0
        }
1388
0
        ExprKind::Block(exprs) => {
1389
0
            for e in exprs {
1390
0
                count_iterator_vs_loops(e, iterators, loops);
1391
0
            }
1392
        }
1393
0
        ExprKind::Function { body, .. } => {
1394
0
            count_iterator_vs_loops(body, iterators, loops);
1395
0
        }
1396
0
        _ => {}
1397
    }
1398
0
}
1399
1400
/// Analyze lambda/closure usage
1401
0
fn analyze_lambda_usage(ast: &crate::frontend::ast::Expr) -> f64 {
1402
0
    let mut lambdas = 0;
1403
0
    let mut total_functions = 0;
1404
    
1405
0
    count_lambda_usage(ast, &mut lambdas, &mut total_functions);
1406
    
1407
0
    if total_functions == 0 {
1408
0
        return 1.0;
1409
0
    }
1410
    
1411
    #[allow(clippy::cast_precision_loss)]
1412
0
    let lambda_ratio = f64::from(lambdas) / f64::from(total_functions);
1413
    
1414
    // Some lambda usage indicates functional style
1415
0
    if lambda_ratio > 0.3 {
1416
0
        1.0
1417
0
    } else if lambda_ratio > 0.1 {
1418
0
        0.95
1419
    } else {
1420
0
        0.9 // Still good if other patterns are used
1421
    }
1422
0
}
1423
1424
0
fn count_lambda_usage(expr: &crate::frontend::ast::Expr, lambdas: &mut i32, total_functions: &mut i32) {
1425
    
1426
0
    match &expr.kind {
1427
0
        ExprKind::Lambda { .. } => {
1428
0
            *lambdas += 1;
1429
0
            *total_functions += 1;
1430
0
        }
1431
0
        ExprKind::Function { body, .. } => {
1432
0
            *total_functions += 1;
1433
0
            count_lambda_usage(body, lambdas, total_functions);
1434
0
        }
1435
0
        ExprKind::Block(exprs) => {
1436
0
            for e in exprs {
1437
0
                count_lambda_usage(e, lambdas, total_functions);
1438
0
            }
1439
        }
1440
0
        _ => {}
1441
    }
1442
0
}