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