use crate::bash_parser::BashParser;
use crate::bash_quality::Formatter;
use crate::bash_transpiler::{PurificationOptions, PurificationReport, Purifier};
use crate::linter::{lint_shell, Diagnostic, LintResult};
pub fn purify_bash(input: &str) -> anyhow::Result<String> {
let mut parser = BashParser::new(input)?;
let ast = parser.parse()?;
let options = PurificationOptions::default();
let mut purifier = Purifier::new(options);
let purified_ast = purifier.purify(&ast)?;
let formatter = Formatter::new();
let purified_code = formatter.format(&purified_ast)?;
Ok(purified_code)
}
#[derive(Debug, Clone)]
pub struct PurifiedLintResult {
pub purified_code: String,
pub lint_result: LintResult,
pub is_clean: bool,
}
impl PurifiedLintResult {
pub fn new(purified_code: String, lint_result: LintResult) -> Self {
let is_clean = Self::check_is_clean(&lint_result);
Self {
purified_code,
lint_result,
is_clean,
}
}
fn check_is_clean(lint_result: &LintResult) -> bool {
!lint_result.diagnostics.iter().any(|d| {
d.code.starts_with("DET") || d.code.starts_with("IDEM") || d.code.starts_with("SEC")
})
}
pub fn critical_violations(&self) -> usize {
self.lint_result
.diagnostics
.iter()
.filter(|d| {
d.code.starts_with("DET") || d.code.starts_with("IDEM") || d.code.starts_with("SEC")
})
.count()
}
pub fn det_violations(&self) -> Vec<&Diagnostic> {
self.lint_result
.diagnostics
.iter()
.filter(|d| d.code.starts_with("DET"))
.collect()
}
pub fn idem_violations(&self) -> Vec<&Diagnostic> {
self.lint_result
.diagnostics
.iter()
.filter(|d| d.code.starts_with("IDEM"))
.collect()
}
pub fn sec_violations(&self) -> Vec<&Diagnostic> {
self.lint_result
.diagnostics
.iter()
.filter(|d| d.code.starts_with("SEC"))
.collect()
}
}
pub fn purify_and_lint(input: &str) -> anyhow::Result<PurifiedLintResult> {
let purified_code = purify_bash(input)?;
let lint_result = lint_shell(&purified_code);
Ok(PurifiedLintResult::new(purified_code, lint_result))
}
pub fn format_purified_lint_result(result: &PurifiedLintResult) -> String {
let mut output = String::new();
output.push_str("Purified:\n");
output.push_str(&result.purified_code);
output.push_str("\n\n");
if result.is_clean {
output.push_str("✓ Purified output is CLEAN (no DET/IDEM/SEC violations)\n");
} else {
output.push_str(&format!(
"✗ Purified output has {} critical violation(s)\n",
result.critical_violations()
));
if !result.det_violations().is_empty() {
output.push_str(&format!(" DET: {}\n", result.det_violations().len()));
}
if !result.idem_violations().is_empty() {
output.push_str(&format!(" IDEM: {}\n", result.idem_violations().len()));
}
if !result.sec_violations().is_empty() {
output.push_str(&format!(" SEC: {}\n", result.sec_violations().len()));
}
}
if !result.lint_result.diagnostics.is_empty() {
output.push_str("\nLint Report:\n");
output.push_str(&crate::repl::linter::format_lint_results(
&result.lint_result,
));
}
output
}
pub fn format_purified_lint_result_with_context(
result: &PurifiedLintResult,
_original_source: &str,
) -> String {
let mut output = String::new();
output.push_str("Purified:\n");
output.push_str(&result.purified_code);
output.push_str("\n\n");
if result.is_clean {
output.push_str("✓ Purified output is CLEAN (no DET/IDEM/SEC violations)\n");
} else {
output.push_str(&format!(
"✗ Purified output has {} critical violation(s)\n",
result.critical_violations()
));
if !result.det_violations().is_empty() {
output.push_str(&format!(" DET: {}\n", result.det_violations().len()));
}
if !result.idem_violations().is_empty() {
output.push_str(&format!(" IDEM: {}\n", result.idem_violations().len()));
}
if !result.sec_violations().is_empty() {
output.push_str(&format!(" SEC: {}\n", result.sec_violations().len()));
}
output.push('\n');
output.push_str(&crate::repl::linter::format_violations_with_context(
&result.lint_result,
&result.purified_code, ));
}
output
}
#[derive(Debug, Clone)]
pub struct PurificationError {
pub purified_code: String,
pub det_violations: usize,
pub idem_violations: usize,
pub sec_violations: usize,
pub diagnostics: Vec<Diagnostic>,
}
impl PurificationError {
pub fn new(result: &PurifiedLintResult) -> Self {
Self {
purified_code: result.purified_code.clone(),
det_violations: result.det_violations().len(),
idem_violations: result.idem_violations().len(),
sec_violations: result.sec_violations().len(),
diagnostics: result.lint_result.diagnostics.clone(),
}
}
pub fn total_violations(&self) -> usize {
self.det_violations + self.idem_violations + self.sec_violations
}
}
impl std::fmt::Display for PurificationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Purified output failed zero-tolerance quality gate: {} violation(s) (DET: {}, IDEM: {}, SEC: {})",
self.total_violations(),
self.det_violations,
self.idem_violations,
self.sec_violations
)
}
}
impl std::error::Error for PurificationError {}
pub fn purify_and_validate(input: &str) -> anyhow::Result<String> {
let result = purify_and_lint(input)?;
if !result.is_clean {
return Err(PurificationError::new(&result).into());
}
Ok(result.purified_code)
}
pub fn format_purification_report(report: &PurificationReport) -> String {
let mut output = String::new();
if !report.idempotency_fixes.is_empty() {
output.push_str("\nIdempotency fixes:\n");
for fix in &report.idempotency_fixes {
output.push_str(&format!(" - {}\n", fix));
}
}
if !report.determinism_fixes.is_empty() {
output.push_str("\nDeterminism fixes:\n");
for fix in &report.determinism_fixes {
output.push_str(&format!(" - {}\n", fix));
}
}
if !report.warnings.is_empty() {
output.push_str("\nWarnings:\n");
for warning in &report.warnings {
output.push_str(&format!(" ⚠ {}\n", warning));
}
}
output
}
pub fn explain_purification_changes(original: &str) -> anyhow::Result<String> {
let purified = purify_bash(original)?;
if original.trim() == purified.trim() {
return Ok("No changes needed - code is already purified.".to_string());
}
let explanations = collect_change_explanations(original, &purified);
if !explanations.is_empty() {
let mut output = String::from("Purification changes:\n\n");
for (i, explanation) in explanations.iter().enumerate() {
if i > 0 {
output.push('\n');
}
output.push_str(explanation);
}
return Ok(output);
}
Ok(format!(
"Changes made during purification:\n\n\
Original:\n {}\n\n\
Purified:\n {}\n\n\
The purified version is more idempotent, deterministic, and safe.",
original.trim(),
purified.trim()
))
}
fn collect_change_explanations(original: &str, purified: &str) -> Vec<String> {
let mut explanations = Vec::new();
if original.contains("mkdir") && !original.contains("mkdir -p") && purified.contains("mkdir -p")
{
explanations.push(
"✓ Added -p flag to mkdir for idempotency\n \
Makes directory creation safe to re-run (won't fail if dir exists)"
.to_string(),
);
}
if original.contains("rm ") && !original.contains("rm -f") && purified.contains("rm -f") {
explanations.push(
"✓ Added -f flag to rm for idempotency\n \
Makes file deletion safe to re-run (won't fail if file doesn't exist)"
.to_string(),
);
}
if original.contains('$') && !original.contains("\"$") && purified.contains("\"$") {
explanations.push(
"✓ Added quotes around variables for safety\n \
Prevents word splitting and glob expansion issues"
.to_string(),
);
}
if original.contains("ln -s") && !original.contains("ln -sf") && purified.contains("ln -sf") {
explanations.push(
"✓ Added -f flag to ln -s for idempotency\n \
Makes symlink creation safe to re-run (forces replacement)"
.to_string(),
);
}
if original.contains("$RANDOM") && !purified.contains("$RANDOM") {
explanations.push(
"✓ Removed $RANDOM for determinism\n \
Non-deterministic values make scripts unpredictable"
.to_string(),
);
}
if (original.contains("date") || original.contains("$SECONDS"))
&& (!purified.contains("date") || !purified.contains("$SECONDS"))
{
explanations.push(
"✓ Removed timestamp for determinism\n \
Time-based values make scripts non-reproducible"
.to_string(),
);
}
explanations
}
pub use super::purifier_transforms::*;
#[cfg(test)]
#[path = "purifier_tests.rs"]
mod tests;