homeboy 0.63.0

CLI for multi-component deployment and development workflow automation
Documentation
use super::conventions::Language;
use super::fixer::{
    apply_insertions_to_content, derive_expected_test_file_path, detect_language,
    extract_expected_test_method_from_fix_description, extract_signatures,
    extract_source_file_from_test_stub, first_failed_detail, mapping_from_source_comment,
    test_method_exists_in_file, Fix, FixKind, FixSafetyTier, Insertion, NewFile, PreflightCheck,
    PreflightContext, PreflightReport, PreflightStatus,
};

pub fn run_insertion_preflight(
    file: &str,
    insertion: &Insertion,
    context: &PreflightContext<'_>,
) -> Option<PreflightReport> {
    match insertion.fix_kind {
        FixKind::MethodStub | FixKind::RegistrationStub | FixKind::ConstructorWithRegistration => {
            let abs_path = context.root.join(file);
            let content = std::fs::read_to_string(&abs_path).ok()?;
            let language = detect_language(&abs_path);
            let simulated =
                apply_insertions_to_content(&content, std::slice::from_ref(insertion), &language);

            let checks = vec![
                collision_check(&content, insertion),
                syntax_shape_check(&simulated, insertion, &language),
            ];
            Some(finalize_report(checks))
        }
        FixKind::MissingTestMethod => {
            let source_file = extract_source_file_from_test_stub(&insertion.description)?;
            let expected_test_method =
                extract_expected_test_method_from_fix_description(&insertion.description)?;
            let expected_test_path = derive_expected_test_file_path(context.root, &source_file)?;

            let checks = vec![
                PreflightCheck {
                    name: "test_mapping".to_string(),
                    passed: file == expected_test_path,
                    detail: if file == expected_test_path {
                        format!("source maps to {}", expected_test_path)
                    } else {
                        format!("source should map to {}, not {}", expected_test_path, file)
                    },
                },
                PreflightCheck {
                    name: "method_collision".to_string(),
                    passed: !test_method_exists_in_file(
                        context.root,
                        file,
                        &expected_test_method,
                        &[],
                    ),
                    detail: if !test_method_exists_in_file(
                        context.root,
                        file,
                        &expected_test_method,
                        &[],
                    ) {
                        format!(
                            "test method {} is not already present",
                            expected_test_method
                        )
                    } else {
                        format!("test method {} already exists", expected_test_method)
                    },
                },
            ];

            Some(finalize_report(checks))
        }
        _ => None,
    }
}

pub fn run_fix_preflight(fix: &mut Fix, context: &PreflightContext<'_>, write: bool) {
    if fix.insertions.is_empty() {
        return;
    }

    let abs_path = context.root.join(&fix.file);
    let Ok(content) = std::fs::read_to_string(&abs_path) else {
        return;
    };
    let language = detect_language(&abs_path);
    let simulated = apply_insertions_to_content(&content, &fix.insertions, &language);

    let mut extra_checks = Vec::new();
    if !fix.required_methods.is_empty() {
        extra_checks.push(required_methods_check(
            &simulated,
            &language,
            &fix.required_methods,
        ));
    }
    if !fix.required_registrations.is_empty() {
        extra_checks.push(required_registrations_check(
            &simulated,
            &fix.required_registrations,
        ));
    }

    for insertion in &mut fix.insertions {
        if insertion.safety_tier != FixSafetyTier::SafeWithChecks {
            continue;
        }

        if let Some(report) = &mut insertion.preflight {
            report.checks.extend(extra_checks.clone());
            *report = finalize_report(report.checks.clone());
        }

        insertion.auto_apply = if !write {
            true
        } else {
            insertion.preflight.as_ref().is_some_and(|report| {
                matches!(
                    report.status,
                    PreflightStatus::Passed | PreflightStatus::NotApplicable
                )
            })
        };

        insertion.blocked_reason = if insertion.auto_apply {
            None
        } else {
            Some(
                insertion
                    .preflight
                    .as_ref()
                    .and_then(first_failed_detail)
                    .unwrap_or_else(|| {
                        "Blocked: requires preflight validation before auto-write".to_string()
                    }),
            )
        };
    }
}

pub fn run_new_file_preflight(
    new_file: &NewFile,
    context: &PreflightContext<'_>,
) -> Option<PreflightReport> {
    match new_file.fix_kind {
        FixKind::MissingTestFile => {
            let (_source_file, expected_test_path) =
                mapping_from_source_comment(&new_file.content)?;
            let abs = context.root.join(&new_file.file);

            Some(finalize_report(vec![
                PreflightCheck {
                    name: "test_mapping".to_string(),
                    passed: expected_test_path == new_file.file,
                    detail: if expected_test_path == new_file.file {
                        format!("source maps to {}", new_file.file)
                    } else {
                        format!(
                            "source should map to {}, not {}",
                            expected_test_path, new_file.file
                        )
                    },
                },
                PreflightCheck {
                    name: "file_absent".to_string(),
                    passed: !abs.exists(),
                    detail: if abs.exists() {
                        format!("{} already exists", new_file.file)
                    } else {
                        format!("{} does not already exist", new_file.file)
                    },
                },
                PreflightCheck {
                    name: "content_nonempty".to_string(),
                    passed: !new_file.content.trim().is_empty(),
                    detail: if new_file.content.trim().is_empty() {
                        "generated test content is empty".to_string()
                    } else {
                        "generated test content is non-empty".to_string()
                    },
                },
            ]))
        }
        _ => None,
    }
}

fn finalize_report(checks: Vec<PreflightCheck>) -> PreflightReport {
    let status = if checks.iter().all(|check| check.passed) {
        PreflightStatus::Passed
    } else {
        PreflightStatus::Failed
    };

    PreflightReport { status, checks }
}

fn collision_check(content: &str, insertion: &Insertion) -> PreflightCheck {
    let collision_free = !content.contains(&insertion.code);
    PreflightCheck {
        name: "collision".to_string(),
        passed: collision_free,
        detail: if collision_free {
            "target file does not already contain this generated code".to_string()
        } else {
            "target file already contains identical generated code".to_string()
        },
    }
}

fn syntax_shape_check(content: &str, insertion: &Insertion, language: &Language) -> PreflightCheck {
    let detail_prefix = match insertion.fix_kind {
        FixKind::MethodStub => "generated method stub",
        FixKind::RegistrationStub => "generated registration update",
        FixKind::ConstructorWithRegistration => "generated constructor",
        _ => "generated content",
    };

    let parsed_ok = match language {
        Language::Php => {
            !super::fixer::extract_php_signatures(content).is_empty() || content.contains("class ")
        }
        Language::Rust => {
            !super::fixer::extract_rust_signatures(content).is_empty() || content.contains("fn ")
        }
        Language::JavaScript | Language::TypeScript => {
            !super::fixer::extract_js_signatures(content).is_empty()
                || content.contains("function ")
        }
        Language::Unknown => true,
    };

    PreflightCheck {
        name: "syntax_shape".to_string(),
        passed: parsed_ok,
        detail: if parsed_ok {
            format!(
                "{} preserves parseable structural signatures",
                detail_prefix
            )
        } else {
            format!(
                "{} produced content that no longer matches expected signature shapes",
                detail_prefix
            )
        },
    }
}

fn required_methods_check(
    content: &str,
    language: &Language,
    required_methods: &[String],
) -> PreflightCheck {
    let found_methods: Vec<String> = extract_signatures(content, language)
        .into_iter()
        .map(|sig| sig.name)
        .collect();

    let missing: Vec<String> = required_methods
        .iter()
        .filter(|method| !found_methods.contains(method))
        .cloned()
        .collect();

    PreflightCheck {
        name: "required_methods".to_string(),
        passed: missing.is_empty(),
        detail: if missing.is_empty() {
            format!(
                "required methods preserved: {}",
                required_methods.join(", ")
            )
        } else {
            format!(
                "missing required methods after simulation: {}",
                missing.join(", ")
            )
        },
    }
}

fn required_registrations_check(
    content: &str,
    required_registrations: &[String],
) -> PreflightCheck {
    let missing: Vec<String> = required_registrations
        .iter()
        .filter(|registration| !content.contains(registration.as_str()))
        .cloned()
        .collect();

    PreflightCheck {
        name: "required_registrations".to_string(),
        passed: missing.is_empty(),
        detail: if missing.is_empty() {
            format!(
                "required registrations preserved: {}",
                required_registrations.join(", ")
            )
        } else {
            format!(
                "missing required registrations after simulation: {}",
                missing.join(", ")
            )
        },
    }
}