1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
//! Ranking utilities for prioritizing analysis results
//!
//! This module provides utilities for ranking and prioritizing files based on
//! analysis results such as complexity metrics, defect counts, and code quality
//! indicators. It helps developers focus on the most problematic areas of code.
//!
//! # Features
//!
//! - **Flexible Ranking**: Configure top-N files and minimum score thresholds
//! - **Metric-based Scoring**: Uses complexity, coverage, and defect metrics
//! - **Severity Mapping**: Automatically maps metrics to severity levels
//! - **Result Builder**: Fluent API for creating analysis results
//! - **Table Formatting**: Pretty-print ranked results for CLI display
//!
//! # Ranking Strategy
//!
//! Files are ranked based on:
//! 1. **Complexity Metrics**: Cyclomatic, cognitive complexity
//! 2. **Defect Severity**: Critical > High > Medium > Low
//! 3. **Defect Count**: Total number of issues found
//! 4. **Coverage Metrics**: Lower coverage increases priority
//!
//! # Example
//!
//! ```no_run
//! use pmat::services::ranking_utils::{RankingConfig, apply_file_ranking, AnalysisResultBuilder};
//! use pmat::services::defect_analyzer::MetricValue;
//! use std::path::PathBuf;
//!
//! # fn example() {
//! // Configure ranking
//! let config = RankingConfig {
//! top_files: 10,
//! min_score: Some(5.0),
//! };
//!
//! // Create analysis results
//! let results = vec![
//! AnalysisResultBuilder::new(PathBuf::from("complex.rs"))
//! .add_metric_int("cyclomatic", 25)
//! .with_entity("calculate", "function")
//! .build(),
//! AnalysisResultBuilder::new(PathBuf::from("simple.rs"))
//! .add_metric_int("cyclomatic", 3)
//! .with_entity("helper", "function")
//! .build(),
//! ];
//!
//! // Apply ranking
//! let ranked = apply_file_ranking(results, &config, |r| r.clone());
//!
//! // Top file should be complex.rs
//! assert_eq!(ranked[0].1, 1); // rank 1
//! # }
//! ```
use crate::models::defect_report::{Defect, DefectCategory, Severity};
use crate::services::defect_analyzer::{
AnalysisContext, AnalysisResult, FileRankingEngine, LineInfo, LineRange, MetricValue,
RankedFile, SimpleScorer,
};
use std::collections::BTreeMap;
use std::path::PathBuf;
/// Configuration for file ranking
#[derive(Default)]
pub struct RankingConfig {
/// Maximum number of files to return (0 = all files)
pub top_files: usize,
/// Minimum score threshold (files below this are excluded)
pub min_score: Option<f64>,
}
/// Apply file ranking to analysis results
pub fn apply_file_ranking<T>(
results: Vec<T>,
config: &RankingConfig,
extractor: impl Fn(&T) -> AnalysisResult,
) -> Vec<(T, usize)> {
if config.top_files == 0 && config.min_score.is_none() {
// No ranking needed, return all with rank 1
return results
.into_iter()
.enumerate()
.map(|(i, r)| (r, i + 1))
.collect();
}
// Convert to defects for ranking
let defects: Vec<Defect> = results
.iter()
.enumerate()
.map(|(i, result)| {
let analysis = extractor(result);
result_to_defect(&analysis, i)
})
.collect();
// Rank files
let engine = FileRankingEngine::new(Box::new(SimpleScorer));
let ranked = engine.rank_files(defects, config.top_files);
// Create a mapping of file paths to ranks
let rank_map: BTreeMap<PathBuf, usize> = ranked.into_iter().map(|r| (r.path, r.rank)).collect();
// Filter and sort results based on ranking
let mut ranked_results: Vec<(T, usize)> = results
.into_iter()
.filter_map(|result| {
let analysis = extractor(&result);
rank_map
.get(&analysis.file_path)
.map(|&rank| (result, rank))
})
.collect();
// Sort by rank
ranked_results.sort_by_key(|(_, rank)| *rank);
ranked_results
}
/// Convert an analysis result to a defect for ranking purposes
fn result_to_defect(result: &AnalysisResult, index: usize) -> Defect {
// Compute severity based on metrics
let severity = compute_severity_from_metrics(&result.metrics);
// Build metrics HashMap
let mut metrics = std::collections::HashMap::new();
for (key, value) in &result.metrics {
if let MetricValue::Float(f) = value {
metrics.insert(key.clone(), *f);
} else if let MetricValue::Integer(i) = value {
metrics.insert(key.clone(), *i as f64);
}
}
Defect {
id: format!("RANK-{index:04}"),
severity,
category: DefectCategory::Complexity, // Default category for ranking
file_path: result.file_path.clone(),
line_start: result.line_range.start.line,
line_end: result.line_range.end.as_ref().map(|e| e.line),
column_start: Some(result.line_range.start.column),
column_end: result.line_range.end.as_ref().map(|e| e.column),
message: result.context.description.clone(),
rule_id: "ranking".to_string(),
fix_suggestion: None,
metrics,
}
}
/// Compute severity from metrics for ranking
fn compute_severity_from_metrics(metrics: &BTreeMap<String, MetricValue>) -> Severity {
// Look for common complexity metrics
let complexity_score = metrics
.iter()
.filter_map(|(k, v)| {
if k.contains("complexity") || k.contains("cyclomatic") || k.contains("cognitive") {
match v {
MetricValue::Integer(i) => Some(*i as f64),
MetricValue::Float(f) => Some(*f),
_ => None,
}
} else {
None
}
})
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0);
if complexity_score > 50.0 {
Severity::Critical
} else if complexity_score > 20.0 {
Severity::High
} else if complexity_score > 10.0 {
Severity::Medium
} else {
Severity::Low
}
}
/// Helper to create analysis results from common patterns
pub struct AnalysisResultBuilder {
file_path: PathBuf,
absolute_path: PathBuf,
line_start: u32,
line_end: Option<u32>,
column_start: u32,
column_end: Option<u32>,
metrics: BTreeMap<String, MetricValue>,
description: String,
entity_name: Option<String>,
entity_type: Option<String>,
}
impl AnalysisResultBuilder {
#[must_use]
pub fn new(file_path: PathBuf) -> Self {
let absolute_path = file_path.clone();
Self {
file_path,
absolute_path,
line_start: 1,
line_end: None,
column_start: 1,
column_end: None,
metrics: BTreeMap::new(),
description: String::new(),
entity_name: None,
entity_type: None,
}
}
#[must_use]
pub fn with_line_range(mut self, start: u32, end: Option<u32>) -> Self {
self.line_start = start;
self.line_end = end;
self
}
#[must_use]
pub fn with_column_range(mut self, start: u32, end: Option<u32>) -> Self {
self.column_start = start;
self.column_end = end;
self
}
pub fn add_metric(mut self, key: impl Into<String>, value: MetricValue) -> Self {
self.metrics.insert(key.into(), value);
self
}
pub fn add_metric_int(mut self, key: impl Into<String>, value: i64) -> Self {
self.metrics.insert(key.into(), MetricValue::Integer(value));
self
}
pub fn add_metric_float(mut self, key: impl Into<String>, value: f64) -> Self {
self.metrics.insert(key.into(), MetricValue::Float(value));
self
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = desc.into();
self
}
pub fn with_entity(mut self, name: impl Into<String>, entity_type: impl Into<String>) -> Self {
self.entity_name = Some(name.into());
self.entity_type = Some(entity_type.into());
self
}
#[must_use]
pub fn build(self) -> AnalysisResult {
AnalysisResult {
file_path: self.file_path,
absolute_path: self.absolute_path,
line_range: LineRange {
start: LineInfo {
line: self.line_start,
column: self.column_start,
byte_offset: 0, // Not computed here
},
end: self.line_end.map(|line| LineInfo {
line,
column: self.column_end.unwrap_or(1),
byte_offset: 0,
}),
},
metrics: self.metrics,
context: AnalysisContext {
description: self.description,
entity_name: self.entity_name,
entity_type: self.entity_type,
},
}
}
}
/// Format a ranked list of files as a table
/// Formats ranked files as a table for display
///
/// # Examples
///
/// ```rust
/// use pmat::services::ranking_utils::format_ranked_files_table;
/// use pmat::services::defect_analyzer::RankedFile;
/// use std::path::PathBuf;
///
/// let files = vec![
/// RankedFile {
/// path: PathBuf::from("src/main.rs"),
/// rank: 1,
/// score: 8.5,
/// defects: vec![],
/// }
/// ];
///
/// let table = format_ranked_files_table(&files);
/// assert!(table.contains("RANK"));
/// assert!(table.contains("src/main.rs"));
/// ```
#[must_use]
pub fn format_ranked_files_table(ranked_files: &[RankedFile]) -> String {
let mut output = String::new();
// Header
output.push_str("RANK SCORE FILE DEFECTS CRITICAL HIGH MEDIUM LOW\n");
output.push_str("---- ------ ------------------------------------------------ ------- -------- ---- ------ ---\n");
for file in ranked_files {
let mut critical = 0;
let mut high = 0;
let mut medium = 0;
let mut low = 0;
for defect in &file.defects {
match defect.severity {
Severity::Critical => critical += 1,
Severity::High => high += 1,
Severity::Medium => medium += 1,
Severity::Low => low += 1,
}
}
output.push_str(&format!(
"{:<4} {:>6.1} {:<48} {:>7} {:>8} {:>4} {:>6} {:>3}\n",
file.rank,
file.score,
file.path
.display()
.to_string()
.chars()
.take(48)
.collect::<String>(),
file.defects.len(),
critical,
high,
medium,
low
));
}
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analysis_result_builder() {
let result = AnalysisResultBuilder::new(PathBuf::from("test.rs"))
.with_line_range(10, Some(20))
.add_metric_int("cyclomatic", 15)
.add_metric_float("coverage", 85.5)
.with_description("Complex function")
.with_entity("process_data", "function")
.build();
assert_eq!(result.file_path, PathBuf::from("test.rs"));
assert_eq!(result.line_range.start.line, 10);
assert_eq!(result.line_range.end.as_ref().unwrap().line, 20);
assert_eq!(
result.metrics.get("cyclomatic"),
Some(&MetricValue::Integer(15))
);
assert_eq!(
result.metrics.get("coverage"),
Some(&MetricValue::Float(85.5))
);
assert_eq!(result.context.entity_name, Some("process_data".to_string()));
}
#[test]
fn test_severity_computation() {
let mut metrics = BTreeMap::new();
// High complexity
metrics.insert("cyclomatic".to_string(), MetricValue::Integer(55));
assert_eq!(compute_severity_from_metrics(&metrics), Severity::Critical);
// Medium complexity
metrics.clear();
metrics.insert("cognitive_complexity".to_string(), MetricValue::Float(15.0));
assert_eq!(compute_severity_from_metrics(&metrics), Severity::Medium);
// Low complexity
metrics.clear();
metrics.insert("complexity".to_string(), MetricValue::Integer(5));
assert_eq!(compute_severity_from_metrics(&metrics), Severity::Low);
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
// Basic property test for coverage
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
// Module consistency verification
prop_assert!(_x < 1001);
}
}
}