batuta/bug_hunter/
modes_hunt.rs1use super::types::*;
4use std::path::Path;
5
6pub(super) fn run_hunt_mode(project_path: &Path, config: &HuntConfig, result: &mut HuntResult) {
8 let crash_patterns = ["crash.log", "panic.log", "*.crash", "stack_trace.txt", "core.*"];
10
11 let mut stack_traces_found = Vec::new();
12
13 for pattern in crash_patterns {
14 if let Ok(entries) = glob::glob(&format!("{}/**/{}", project_path.display(), pattern)) {
15 for entry in entries.flatten() {
16 stack_traces_found.push(entry);
17 }
18 }
19 }
20
21 let test_output = project_path.join("target/nextest/ci/junit.xml");
23 if test_output.exists() {
24 if let Ok(content) = std::fs::read_to_string(&test_output) {
25 if content.contains("panicked") || content.contains("FAILED") {
26 stack_traces_found.push(test_output);
27 }
28 }
29 }
30
31 if stack_traces_found.is_empty() {
32 analyze_coverage_hotspots(project_path, config, result);
34 } else {
35 for trace_file in stack_traces_found {
36 analyze_stack_trace(&trace_file, project_path, config, result);
37 }
38 }
39}
40
41pub(super) fn analyze_coverage_hotspots(
43 project_path: &Path,
44 config: &HuntConfig,
45 result: &mut HuntResult,
46) {
47 if let Some(ref custom_path) = config.coverage_path {
49 if custom_path.exists() {
50 if let Ok(content) = std::fs::read_to_string(custom_path) {
51 parse_lcov_for_hotspots(&content, project_path, result);
52 return;
53 }
54 }
55 }
56
57 let mut lcov_paths: Vec<std::path::PathBuf> = vec![
59 project_path.join("lcov.info"),
61 project_path.join("target/coverage/lcov.info"),
63 project_path.join("target/llvm-cov/lcov.info"),
64 project_path.join("coverage/lcov.info"),
65 ];
66
67 if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") {
69 let target_path = std::path::Path::new(&target_dir);
70 lcov_paths.push(target_path.join("coverage/lcov.info"));
71 lcov_paths.push(target_path.join("llvm-cov/lcov.info"));
72 }
73
74 for lcov_path in &lcov_paths {
75 if lcov_path.exists() {
76 if let Ok(content) = std::fs::read_to_string(lcov_path) {
77 parse_lcov_for_hotspots(&content, project_path, result);
78 return;
79 }
80 }
81 }
82
83 let searched =
85 lcov_paths.iter().map(|p| p.display().to_string()).collect::<Vec<_>>().join(", ");
86 result.add_finding(
87 Finding::new(
88 "BH-HUNT-NOCOV",
89 project_path.join("target"),
90 1,
91 "No coverage data available",
92 )
93 .with_description(format!(
94 "Run `cargo llvm-cov --lcov --output-path lcov.info` or use --coverage-path. Searched: {}",
95 searched
96 ))
97 .with_severity(FindingSeverity::Info)
98 .with_category(DefectCategory::ConfigurationErrors)
99 .with_suspiciousness(0.1)
100 .with_discovered_by(HuntMode::Hunt),
101 );
102}
103
104pub(super) fn parse_lcov_da_line(
106 da: &str,
107 file: &str,
108 file_uncovered: &mut std::collections::HashMap<String, Vec<usize>>,
109) {
110 let Some((line_str, hits_str)) = da.split_once(',') else {
111 return;
112 };
113 let Ok(line_num) = line_str.parse::<usize>() else {
114 return;
115 };
116 let Ok(hits) = hits_str.parse::<usize>() else {
117 return;
118 };
119 if hits == 0 {
120 file_uncovered.entry(file.to_string()).or_default().push(line_num);
121 }
122}
123
124pub(super) fn report_uncovered_hotspots(
126 file_uncovered: std::collections::HashMap<String, Vec<usize>>,
127 project_path: &Path,
128 result: &mut HuntResult,
129) {
130 let mut finding_id = 0;
131 for (file, lines) in file_uncovered {
132 if lines.len() <= 5 {
133 continue;
134 }
135 finding_id += 1;
136 let suspiciousness = (lines.len() as f64 / 100.0).min(0.8);
137 result.add_finding(
138 Finding::new(
139 format!("BH-COV-{:04}", finding_id),
140 project_path.join(&file),
141 lines[0],
142 format!("Low coverage region ({} uncovered lines)", lines.len()),
143 )
144 .with_description(format!(
145 "Lines {} are never executed; potential dead code or missing tests",
146 lines.iter().take(5).map(|l| l.to_string()).collect::<Vec<_>>().join(", ")
147 ))
148 .with_severity(FindingSeverity::Low)
149 .with_category(DefectCategory::LogicErrors)
150 .with_suspiciousness(suspiciousness)
151 .with_discovered_by(HuntMode::Hunt)
152 .with_evidence(FindingEvidence::sbfl("Coverage", suspiciousness)),
153 );
154 }
155}
156
157pub(super) fn parse_lcov_for_hotspots(content: &str, project_path: &Path, result: &mut HuntResult) {
159 let mut current_file: Option<String> = None;
160 let mut file_uncovered: std::collections::HashMap<String, Vec<usize>> =
161 std::collections::HashMap::new();
162
163 for line in content.lines() {
164 if let Some(file) = line.strip_prefix("SF:") {
165 current_file = Some(file.to_string());
166 } else if let Some(da) = line.strip_prefix("DA:") {
167 if let Some(ref file) = current_file {
168 parse_lcov_da_line(da, file, &mut file_uncovered);
169 }
170 } else if line == "end_of_record" {
171 current_file = None;
172 }
173 }
174
175 report_uncovered_hotspots(file_uncovered, project_path, result);
176}
177
178pub(super) fn analyze_stack_trace(
180 trace_file: &Path,
181 _project_path: &Path,
182 _config: &HuntConfig,
183 result: &mut HuntResult,
184) {
185 let content = match std::fs::read_to_string(trace_file) {
186 Ok(c) => c,
187 Err(_) => return,
188 };
189
190 let mut finding_id = 0;
191
192 for line in content.lines() {
194 if let Some(at_pos) = line.find(" at ") {
196 let location = &line[at_pos + 4..];
197 if let Some(colon_pos) = location.rfind(':') {
198 let file = &location[..colon_pos];
199 if let Ok(line_num) = location[colon_pos + 1..].trim().parse::<usize>() {
200 if file.ends_with(".rs") && !file.contains("/.cargo/") {
201 finding_id += 1;
202 result.add_finding(
203 Finding::new(
204 format!("BH-STACK-{:04}", finding_id),
205 file,
206 line_num,
207 "Stack trace location",
208 )
209 .with_description(format!(
210 "Found in stack trace: {}",
211 trace_file.display()
212 ))
213 .with_severity(FindingSeverity::High)
214 .with_category(DefectCategory::LogicErrors)
215 .with_suspiciousness(0.85)
216 .with_discovered_by(HuntMode::Hunt)
217 .with_evidence(FindingEvidence::sbfl("StackTrace", 0.85)),
218 );
219 }
220 }
221 }
222 }
223 }
224}