use crate::make_parser::ast::{MakeAst, MakeItem};
use crate::make_parser::semantic::{analyze_makefile, SemanticIssue};
mod error_handling;
mod parallel_safety;
mod performance;
mod portability;
mod report;
mod reproducible_builds;
#[cfg(test)]
mod tests;
#[cfg(test)]
#[path = "purify_tests.rs"]
mod purify_tests;
#[derive(Debug, Clone, PartialEq)]
pub struct PurificationResult {
pub ast: MakeAst,
pub transformations_applied: usize,
pub issues_fixed: usize,
pub manual_fixes_needed: usize,
pub report: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Transformation {
WrapWithSort {
variable_name: String,
pattern: String,
safe: bool,
},
AddComment {
variable_name: String,
rule: String,
suggestion: String,
safe: bool,
},
RecommendNotParallel { reason: String, safe: bool },
DetectRaceCondition {
target_names: Vec<String>,
conflicting_file: String,
safe: bool,
},
RecommendOrderOnlyPrereq {
target_name: String,
prereq_name: String,
reason: String,
safe: bool,
},
DetectMissingDependency {
target_name: String,
missing_file: String,
provider_target: String,
safe: bool,
},
DetectOutputConflict {
target_names: Vec<String>,
output_file: String,
safe: bool,
},
RecommendRecursiveMakeHandling {
target_name: String,
subdirs: Vec<String>,
safe: bool,
},
DetectDirectoryRace {
target_names: Vec<String>,
directory: String,
safe: bool,
},
DetectTimestamp {
variable_name: String,
pattern: String,
safe: bool,
},
DetectRandom { variable_name: String, safe: bool },
DetectProcessId { variable_name: String, safe: bool },
SuggestSourceDateEpoch {
variable_name: String,
original_pattern: String,
safe: bool,
},
DetectNonDeterministicCommand {
variable_name: String,
command: String,
reason: String,
safe: bool,
},
SuggestCombineShellInvocations {
target_name: String,
recipe_count: usize,
safe: bool,
},
SuggestSimpleExpansion {
variable_name: String,
reason: String,
safe: bool,
},
RecommendSuffixes { reason: String, safe: bool },
DetectSequentialRecipes {
target_name: String,
recipe_count: usize,
safe: bool,
},
SuggestPatternRule {
pattern: String,
target_count: usize,
safe: bool,
},
DetectMissingErrorHandling {
target_name: String,
command: String,
safe: bool,
},
DetectSilentFailure {
target_name: String,
command: String,
safe: bool,
},
RecommendDeleteOnError { reason: String, safe: bool },
RecommendOneshell {
target_name: String,
reason: String,
safe: bool,
},
DetectMissingSetE {
target_name: String,
command: String,
safe: bool,
},
DetectLoopWithoutErrorHandling {
target_name: String,
loop_command: String,
safe: bool,
},
DetectBashism {
target_name: String,
construct: String,
posix_alternative: String,
safe: bool,
},
DetectPlatformSpecific {
target_name: String,
command: String,
reason: String,
safe: bool,
},
DetectShellSpecific {
target_name: String,
feature: String,
posix_alternative: String,
safe: bool,
},
DetectNonPortableFlags {
target_name: String,
command: String,
flag: String,
reason: String,
safe: bool,
},
DetectNonPortableEcho {
target_name: String,
command: String,
safe: bool,
},
}
pub fn purify_makefile(ast: &MakeAst) -> PurificationResult {
let issues = analyze_makefile(ast);
let mut transformations = plan_transformations(ast, &issues);
transformations.extend(parallel_safety::analyze_parallel_safety(ast));
transformations.extend(reproducible_builds::analyze_reproducible_builds(ast));
transformations.extend(performance::analyze_performance_optimization(ast));
transformations.extend(error_handling::analyze_error_handling(ast));
transformations.extend(portability::analyze_portability(ast));
let purified_ast = apply_transformations(ast, &transformations);
let issues_fixed = transformations
.iter()
.filter(|t| report::is_safe_transformation(t))
.count();
let manual_fixes_needed = transformations
.iter()
.filter(|t| !report::is_safe_transformation(t))
.count();
let report = report::generate_report(&transformations);
PurificationResult {
ast: purified_ast,
transformations_applied: transformations.len(),
issues_fixed,
manual_fixes_needed,
report,
}
}
fn plan_transformations(_ast: &MakeAst, issues: &[SemanticIssue]) -> Vec<Transformation> {
let mut transformations = Vec::new();
for issue in issues {
let var_name = extract_variable_name(&issue.message);
match issue.rule.as_str() {
"NO_WILDCARD" => {
transformations.push(Transformation::WrapWithSort {
variable_name: var_name,
pattern: "$(wildcard".to_string(),
safe: true,
});
}
"NO_UNORDERED_FIND" => {
transformations.push(Transformation::WrapWithSort {
variable_name: var_name,
pattern: "$(shell find".to_string(),
safe: true,
});
}
"NO_TIMESTAMPS" => {
transformations.push(Transformation::AddComment {
variable_name: var_name,
rule: issue.rule.clone(),
suggestion: issue.suggestion.clone().unwrap_or_default(),
safe: false, });
}
"NO_RANDOM" => {
transformations.push(Transformation::AddComment {
variable_name: var_name,
rule: issue.rule.clone(),
suggestion: issue.suggestion.clone().unwrap_or_default(),
safe: false, });
}
_ => {}
}
}
transformations
}
fn apply_transformations(ast: &MakeAst, transformations: &[Transformation]) -> MakeAst {
let mut purified = ast.clone();
for transformation in transformations {
if let Transformation::WrapWithSort {
variable_name,
pattern,
..
} = transformation
{
wrap_variable_with_sort(&mut purified, variable_name, pattern);
}
}
purified
}
fn wrap_variable_with_sort(ast: &mut MakeAst, variable_name: &str, pattern: &str) {
for item in &mut ast.items {
if let MakeItem::Variable { name, value, .. } = item {
if name == variable_name && value.contains(pattern) {
*value = wrap_pattern_with_sort(value, pattern);
}
}
}
}
fn wrap_pattern_with_sort(value: &str, pattern: &str) -> String {
if let Some(start) = value.find(pattern) {
if let Some(end) = find_matching_paren(value, start) {
let complete_pattern = &value[start..=end];
let wrapped = format!("$(sort {})", complete_pattern);
value.replace(complete_pattern, &wrapped)
} else {
value.to_string()
}
} else {
value.to_string()
}
}
fn find_matching_paren(s: &str, start: usize) -> Option<usize> {
let bytes = s.as_bytes();
let mut depth = 0;
let mut found_opening = false;
let mut i = start;
while i < bytes.len() {
if bytes.get(i) == Some(&b'(') && i > 0 && bytes.get(i - 1) == Some(&b'$') {
depth = 1;
found_opening = true;
i += 1;
break;
}
i += 1;
}
if !found_opening {
return None;
}
while i < bytes.len() {
match bytes.get(i) {
Some(&b'$') if bytes.get(i + 1) == Some(&b'(') => {
depth += 1;
i += 2; continue;
}
Some(&b')') => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
i += 1;
}
None
}
fn extract_variable_name(message: &str) -> String {
if let Some(start) = message.find('\'') {
if let Some(end) = message[start + 1..].find('\'') {
return message[start + 1..start + 1 + end].to_string();
}
}
String::new()
}