1use crate::ast_analysis::{filter_mutatable_lines, AridNodeDetector};
2use crate::error::{MutationError, Result};
3use crate::git_changes::{get_changed_files, get_lines_touched};
4use crate::operators::{
5 get_do_not_mutate_patterns, get_do_not_mutate_py_patterns, get_do_not_mutate_unit_patterns,
6 get_regex_operators, get_security_operators, get_skip_if_contain_patterns, get_test_operators,
7 should_mutate_test_line,
8};
9use regex::Regex;
10use std::collections::HashMap;
11use std::fs;
12use std::path::{Path, PathBuf};
13
14#[derive(Debug)]
15pub struct FileToMutate {
16 pub file_path: String,
17 pub lines_touched: Vec<usize>,
18 pub is_unit_test: bool,
19}
20
21pub async fn run_mutation(
22 pr_number: Option<u32>,
23 file: Option<PathBuf>,
24 one_mutant: bool,
25 only_security_mutations: bool,
26 range_lines: Option<(usize, usize)>,
27 coverage: Option<HashMap<String, Vec<usize>>>,
28 test_only: bool,
29 skip_lines: HashMap<String, Vec<usize>>,
30 enable_ast_filtering: bool,
31 custom_expert_rule: Option<String>,
32) -> Result<()> {
33 if let Some(file_path) = file {
34 let file_str = file_path.to_string_lossy().to_string();
35 let is_unit_test = file_str.contains("test") && !file_str.contains(".py");
36
37 mutate_file(
38 &file_str,
39 None,
40 None,
41 one_mutant,
42 only_security_mutations,
43 range_lines,
44 &coverage,
45 is_unit_test,
46 &skip_lines,
47 enable_ast_filtering,
48 custom_expert_rule,
49 )
50 .await?;
51 return Ok(());
52 }
53
54 let files_changed = get_changed_files(pr_number).await?;
55 let mut files_to_mutate = Vec::new();
56
57 for file_changed in files_changed {
58 if file_changed.contains("doc")
60 || file_changed.contains("fuzz")
61 || file_changed.contains("bench")
62 || file_changed.contains("util")
63 || file_changed.contains("sanitizer_supressions")
64 || file_changed.ends_with(".txt")
65 {
66 continue;
67 }
68
69 let lines_touched = get_lines_touched(&file_changed).await?;
70 let is_unit_test = file_changed.contains("test")
71 && !file_changed.contains(".py")
72 && !file_changed.contains("util");
73
74 if test_only && !(is_unit_test || file_changed.contains(".py")) {
75 continue;
76 }
77
78 files_to_mutate.push(FileToMutate {
79 file_path: file_changed,
80 lines_touched,
81 is_unit_test,
82 });
83 }
84
85 for file_info in files_to_mutate {
86 mutate_file(
87 &file_info.file_path,
88 Some(file_info.lines_touched),
89 pr_number,
90 one_mutant,
91 only_security_mutations,
92 range_lines,
93 &coverage,
94 file_info.is_unit_test,
95 &skip_lines,
96 enable_ast_filtering,
97 custom_expert_rule.clone(),
98 )
99 .await?;
100 }
101
102 Ok(())
103}
104
105pub async fn mutate_file(
106 file_to_mutate: &str,
107 touched_lines: Option<Vec<usize>>,
108 pr_number: Option<u32>,
109 one_mutant: bool,
110 only_security_mutations: bool,
111 range_lines: Option<(usize, usize)>,
112 coverage: &Option<HashMap<String, Vec<usize>>>,
113 is_unit_test: bool,
114 skip_lines: &HashMap<String, Vec<usize>>,
115 enable_ast_filtering: bool,
116 custom_expert_rule: Option<String>,
117) -> Result<()> {
118 println!("\n\nGenerating mutants for {}...", file_to_mutate);
119
120 let source_code = fs::read_to_string(file_to_mutate)?;
121 let lines: Vec<&str> = source_code.lines().collect();
122 println!("File has {} lines", lines.len());
123
124 let mut arid_detector = if enable_ast_filtering
126 && (file_to_mutate.ends_with(".cpp") || file_to_mutate.ends_with(".h"))
127 {
128 let mut detector = AridNodeDetector::new()?;
129
130 if let Some(rule) = custom_expert_rule {
132 detector.add_expert_rule(&rule, "Custom user rule")?;
133 }
134
135 Some(detector)
136 } else {
137 if !enable_ast_filtering {
138 println!("AST filtering disabled - generating all possible mutants");
139 }
140 None
141 };
142
143 let ast_filtered_lines = if let Some(ref mut detector) = arid_detector {
145 let string_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
146 let mutatable_line_numbers = filter_mutatable_lines(&string_lines, detector);
147 println!(
148 "AST analysis filtered to {} mutatable lines (from {})",
149 mutatable_line_numbers.len(),
150 lines.len()
151 );
152
153 let filtered_out_count = lines.len() - mutatable_line_numbers.len();
155 if filtered_out_count > 0 {
156 println!(
157 "Filtered out {} arid lines (logging, reserve calls, etc.)",
158 filtered_out_count
159 );
160 }
161
162 Some(mutatable_line_numbers)
163 } else {
164 None
165 };
166
167 let operators = if only_security_mutations {
169 println!("Using security operators");
170 get_security_operators()?
171 } else if file_to_mutate.contains(".py") || is_unit_test {
172 println!("Using test operators (Python or unit test file)");
173 get_test_operators()?
174 } else {
175 println!("Using regex operators");
176 get_regex_operators()?
177 };
178
179 println!("Loaded {} operators", operators.len());
180
181 let skip_lines_for_file = skip_lines.get(file_to_mutate);
182 let mut touched_lines = touched_lines.unwrap_or_else(|| (1..=lines.len()).collect());
183
184 if let Some(ast_lines) = ast_filtered_lines {
186 touched_lines.retain(|line_num| ast_lines.contains(line_num));
188 println!(
189 "After AST filtering: {} lines to process",
190 touched_lines.len()
191 );
192 }
193
194 let lines_with_test_coverage = if let Some(cov) = coverage {
196 cov.iter()
197 .find(|(path, _)| file_to_mutate.contains(path.as_str()))
198 .map(|(_, lines)| lines.clone())
199 .unwrap_or_default()
200 } else {
201 Vec::new()
202 };
203
204 if !lines_with_test_coverage.is_empty() {
205 println!(
206 "Using coverage data with {} covered lines",
207 lines_with_test_coverage.len()
208 );
209 }
210
211 let mut mutant_count = 0;
212
213 if one_mutant {
214 println!("One mutant mode enabled");
215 }
216
217 for line_num in touched_lines {
218 let line_idx = line_num.saturating_sub(1);
219
220 if !lines_with_test_coverage.is_empty() && !lines_with_test_coverage.contains(&line_num) {
222 continue;
223 }
224
225 if let Some((start, end)) = range_lines {
227 if line_idx < start || line_idx > end {
228 continue;
229 }
230 }
231
232 if let Some(skip) = skip_lines_for_file {
234 if skip.contains(&line_num) {
235 continue;
236 }
237 }
238
239 if line_idx >= lines.len() {
240 continue;
241 }
242
243 let line_before_mutation = lines[line_idx];
244
245 if should_skip_line(line_before_mutation, file_to_mutate, is_unit_test)? {
247 continue;
248 }
249
250 let mut line_had_match = false;
251
252 for operator in &operators {
253 if file_to_mutate.contains(".py") || is_unit_test {
255 if !should_mutate_test_line(line_before_mutation) {
256 continue;
257 }
258 }
259
260 if operator.pattern.is_match(line_before_mutation) {
261 line_had_match = true;
262 let line_mutated = operator
263 .pattern
264 .replace(line_before_mutation, &operator.replacement);
265
266 let mut mutated_lines = lines.clone();
268 mutated_lines[line_idx] = &line_mutated;
269 let mutated_content = mutated_lines.join("\n");
270
271 mutant_count = write_mutation(
272 file_to_mutate,
273 &mutated_content,
274 mutant_count,
275 pr_number,
276 range_lines,
277 )?;
278
279 if one_mutant {
280 break; }
282 }
283 }
284
285 if !line_had_match && !line_before_mutation.trim().is_empty() {
287 println!(
288 "Line {} '{}' didn't match any patterns",
289 line_num,
290 line_before_mutation.trim()
291 );
292 }
293
294 }
297
298 if let Some(detector) = arid_detector {
300 let stats = detector.get_stats();
301 println!("AST Analysis Stats: {:?}", stats);
302 }
303
304 println!("Generated {} mutants...", mutant_count);
305 Ok(())
306}
307
308fn should_skip_line(line: &str, file_path: &str, is_unit_test: bool) -> Result<bool> {
309 let trimmed = line.trim_start();
310
311 for pattern in get_do_not_mutate_patterns() {
313 if trimmed.starts_with(pattern) {
314 return Ok(true);
315 }
316 }
317
318 for pattern in get_skip_if_contain_patterns() {
320 if line.contains(pattern) {
321 return Ok(true);
322 }
323 }
324
325 if file_path.contains(".py") || is_unit_test {
327 let patterns = if is_unit_test {
328 get_do_not_mutate_unit_patterns()
329 } else {
330 get_do_not_mutate_py_patterns()
331 };
332
333 for pattern in patterns {
334 if line.contains(pattern) {
335 return Ok(true);
336 }
337 }
338
339 let assignment_regex = if is_unit_test {
341 Regex::new(
342 r"\b(?:[a-zA-Z_][a-zA-Z0-9_:<>*&\s]+)\s+[a-zA-Z_][a-zA-Z0-9_]*(?:\[[^\]]*\])?(?:\.(?:[a-zA-Z_][a-zA-Z0-9_]*)|\->(?:[a-zA-Z_][a-zA-Z0-9_]*))*(?:\s*=\s*[^;]+|\s*\{[^;]+\})\s*",
343 )?
344 } else {
345 Regex::new(r"^\s*([a-zA-Z_]\w*)\s*=\s*(.+)$")?
346 };
347
348 if assignment_regex.is_match(line) {
349 return Ok(true);
350 }
351 }
352
353 Ok(false)
354}
355
356fn write_mutation(
357 file_to_mutate: &str,
358 mutated_content: &str,
359 mutant_index: usize,
360 pr_number: Option<u32>,
361 range_lines: Option<(usize, usize)>,
362) -> Result<usize> {
363 let file_extension = if file_to_mutate.ends_with(".h") {
364 ".h"
365 } else if file_to_mutate.ends_with(".py") {
366 ".py"
367 } else {
368 ".cpp"
369 };
370
371 let file_name = Path::new(file_to_mutate)
372 .file_stem()
373 .and_then(|s| s.to_str())
374 .ok_or_else(|| MutationError::InvalidInput("Invalid file path".to_string()))?;
375
376 let ext = file_extension.trim_start_matches('.');
377 let folder = if let Some(pr) = pr_number {
378 format!("muts-pr-{}-{}-{}", pr, file_name, ext)
379 } else if let Some(range) = range_lines {
380 format!("muts-pr-{}-{}-{}", file_name, range.0, range.1)
381 } else {
382 format!("muts-{}-{}", file_name, ext)
383 };
384
385 create_mutation_folder(&folder, file_to_mutate)?;
386
387 let mutator_file = format!(
388 "{}/{}.mutant.{}{}",
389 folder, file_name, mutant_index, file_extension
390 );
391 fs::write(mutator_file, mutated_content)?;
392
393 Ok(mutant_index + 1)
394}
395
396fn create_mutation_folder(folder_name: &str, file_to_mutate: &str) -> Result<()> {
397 let folder_path = Path::new(folder_name);
398
399 if !folder_path.exists() {
400 fs::create_dir_all(folder_path)?;
401
402 let original_file_path = folder_path.join("original_file.txt");
403 fs::write(original_file_path, file_to_mutate)?;
404 }
405
406 Ok(())
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412 use tempfile::tempdir;
413
414 #[test]
415 fn test_should_skip_line() {
416 assert!(should_skip_line("// This is a comment", "test.cpp", false).unwrap());
418 assert!(should_skip_line("assert(condition);", "test.cpp", false).unwrap());
419 assert!(should_skip_line("LogPrintf(\"test\");", "test.cpp", false).unwrap());
420 assert!(should_skip_line("LogDebug(\"test\");", "test.cpp", false).unwrap());
421
422 assert!(!should_skip_line("int x = 5;", "test.cpp", false).unwrap());
424 assert!(!should_skip_line("return value;", "test.cpp", false).unwrap());
425 }
426
427 #[test]
428 fn test_create_mutation_folder() {
429 let temp_dir = tempdir().unwrap();
430 let folder_path = temp_dir.path().join("test_muts");
431 let folder_name = folder_path.to_str().unwrap();
432
433 create_mutation_folder(folder_name, "test/file.cpp").unwrap();
434
435 assert!(folder_path.exists());
436 assert!(folder_path.join("original_file.txt").exists());
437
438 let content = fs::read_to_string(folder_path.join("original_file.txt")).unwrap();
439 assert_eq!(content, "test/file.cpp");
440 }
441
442 #[test]
443 fn test_write_mutation() {
444 let temp_dir = tempdir().unwrap();
445 std::env::set_current_dir(&temp_dir).unwrap();
446
447 let result = write_mutation("test.cpp", "mutated content", 0, None, None).unwrap();
448 assert_eq!(result, 1);
449
450 let folder_path = Path::new("muts-test-cpp");
451 assert!(folder_path.exists());
452 assert!(folder_path.join("test.mutant.0.cpp").exists());
453
454 let content = fs::read_to_string(folder_path.join("test.mutant.0.cpp")).unwrap();
455 assert_eq!(content, "mutated content");
456 }
457}