use std::collections::HashMap;
use std::path::Path;
use regex::Regex;
use super::conventions::{DeviationKind, Language};
use super::CodeAuditResult;
#[derive(Debug, Clone, serde::Serialize)]
pub struct Fix {
pub file: String,
pub insertions: Vec<Insertion>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub applied: bool,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct Insertion {
pub kind: InsertionKind,
pub code: String,
pub description: String,
}
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InsertionKind {
MethodStub,
RegistrationStub,
ConstructorWithRegistration,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct SkippedFile {
pub file: String,
pub reason: String,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct FixResult {
pub fixes: Vec<Fix>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub skipped: Vec<SkippedFile>,
pub total_insertions: usize,
pub files_modified: usize,
}
#[derive(Debug, Clone)]
pub(super) struct MethodSignature {
pub(super) name: String,
pub(super) signature: String,
#[allow(dead_code)]
pub(super) language: Language,
}
pub(super) fn extract_signatures(content: &str, language: &Language) -> Vec<MethodSignature> {
match language {
Language::Php => extract_php_signatures(content),
Language::Rust => extract_rust_signatures(content),
Language::JavaScript | Language::TypeScript => extract_js_signatures(content),
Language::Unknown => vec![],
}
}
fn extract_php_signatures(content: &str) -> Vec<MethodSignature> {
let re = Regex::new(
r"(?m)^\s*((?:public|protected|private)\s+(?:static\s+)?function\s+(\w+)\s*\([^)]*\)(?:\s*:\s*[\w\\|?]+)?)",
)
.unwrap();
re.captures_iter(content)
.map(|cap| MethodSignature {
name: cap[2].to_string(),
signature: cap[1].trim().to_string(),
language: Language::Php,
})
.collect()
}
fn extract_rust_signatures(content: &str) -> Vec<MethodSignature> {
let re = Regex::new(
r"(?m)^\s*(pub(?:\(crate\))?\s+(?:async\s+)?fn\s+(\w+)\s*\([^)]*\)(?:\s*->\s*[^\{]+)?)",
)
.unwrap();
re.captures_iter(content)
.map(|cap| MethodSignature {
name: cap[2].to_string(),
signature: cap[1].trim().to_string(),
language: Language::Rust,
})
.collect()
}
fn extract_js_signatures(content: &str) -> Vec<MethodSignature> {
let fn_re = Regex::new(
r"(?m)^\s*((?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\))",
)
.unwrap();
let method_re = Regex::new(
r"(?m)^\s+((?:async\s+)?(\w+)\s*\([^)]*\))\s*\{",
)
.unwrap();
let mut sigs: Vec<MethodSignature> = fn_re
.captures_iter(content)
.map(|cap| MethodSignature {
name: cap[2].to_string(),
signature: cap[1].trim().to_string(),
language: Language::JavaScript,
})
.collect();
let skip = ["if", "for", "while", "switch", "catch", "return"];
for cap in method_re.captures_iter(content) {
let name = cap[2].to_string();
if !skip.contains(&name.as_str()) && !sigs.iter().any(|s| s.name == name) {
sigs.push(MethodSignature {
name,
signature: cap[1].trim().to_string(),
language: Language::JavaScript,
});
}
}
sigs
}
fn stub_body(method_name: &str, language: &Language) -> String {
match language {
Language::Php => {
format!(
" throw new \\RuntimeException('Not implemented: {}');",
method_name
)
}
Language::Rust => {
format!(" todo!(\"{}\")", method_name)
}
Language::JavaScript | Language::TypeScript => {
format!(
" throw new Error('Not implemented: {}');",
method_name
)
}
Language::Unknown => String::new(),
}
}
fn generate_method_stub(sig: &MethodSignature) -> String {
let body = stub_body(&sig.name, &sig.language);
match sig.language {
Language::Php => {
format!("\n {} {{\n{}\n }}\n", sig.signature, body)
}
Language::Rust => {
format!("\n {} {{\n{}\n }}\n", sig.signature, body)
}
Language::JavaScript | Language::TypeScript => {
format!("\n {} {{\n{}\n }}\n", sig.signature, body)
}
Language::Unknown => String::new(),
}
}
fn generate_registration_stub(hook_name: &str) -> String {
let callback = hook_name
.strip_prefix("wp_")
.or_else(|| hook_name.strip_prefix("datamachine_"))
.unwrap_or(hook_name);
format!(" add_action('{}', [$this, '{}']);", hook_name, callback)
}
fn build_signature_map(
conforming_files: &[String],
root: &Path,
) -> HashMap<String, MethodSignature> {
let mut sig_map: HashMap<String, MethodSignature> = HashMap::new();
for rel_path in conforming_files {
let abs_path = root.join(rel_path);
if let Ok(content) = std::fs::read_to_string(&abs_path) {
let language = abs_path
.extension()
.and_then(|e| e.to_str())
.map(Language::from_extension)
.unwrap_or(Language::Unknown);
for sig in extract_signatures(&content, &language) {
sig_map.entry(sig.name.clone()).or_insert(sig);
}
}
}
sig_map
}
fn detect_language(path: &Path) -> Language {
path.extension()
.and_then(|e| e.to_str())
.map(Language::from_extension)
.unwrap_or(Language::Unknown)
}
fn file_has_constructor(content: &str, language: &Language) -> bool {
match language {
Language::Php => content.contains("function __construct"),
Language::Rust => content.contains("fn new("),
Language::JavaScript | Language::TypeScript => content.contains("constructor("),
Language::Unknown => false,
}
}
pub fn generate_fixes(result: &CodeAuditResult, root: &Path) -> FixResult {
let mut fixes = Vec::new();
let mut skipped = Vec::new();
for conv_report in &result.conventions {
if conv_report.outliers.is_empty() {
continue;
}
if conv_report.confidence < 0.5 {
for outlier in &conv_report.outliers {
skipped.push(SkippedFile {
file: outlier.file.clone(),
reason: format!(
"Convention '{}' confidence too low ({:.0}%) — needs manual review",
conv_report.name,
conv_report.confidence * 100.0
),
});
}
continue;
}
let naming_suffix = detect_naming_suffix(&conv_report.conforming);
let sig_map = build_signature_map(&conv_report.conforming, root);
for outlier in &conv_report.outliers {
if let Some(ref suffix) = naming_suffix {
let file_stem = Path::new(&outlier.file)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
if !suffix_matches(&file_stem, suffix) {
skipped.push(SkippedFile {
file: outlier.file.clone(),
reason: format!(
"Name doesn't match convention pattern '*{}' — likely a utility/helper, needs manual refactoring",
suffix
),
});
continue;
}
}
let mut insertions = Vec::new();
let abs_path = root.join(&outlier.file);
let language = detect_language(&abs_path);
let content = std::fs::read_to_string(&abs_path).unwrap_or_default();
let has_constructor = file_has_constructor(&content, &language);
let mut missing_methods: Vec<&str> = Vec::new();
let mut missing_registrations: Vec<&str> = Vec::new();
let mut needs_constructor = false;
for deviation in &outlier.deviations {
match deviation.kind {
DeviationKind::MissingMethod => {
let method_name = deviation
.description
.strip_prefix("Missing method: ")
.unwrap_or(&deviation.description);
if method_name.len() < 3 {
continue;
}
if method_name == "__construct" || method_name == "new" || method_name == "constructor" {
needs_constructor = true;
} else {
missing_methods.push(method_name);
}
}
DeviationKind::MissingRegistration => {
let hook_name = deviation
.description
.strip_prefix("Missing registration: ")
.unwrap_or(&deviation.description);
missing_registrations.push(hook_name);
}
_ => {}
}
}
if !missing_registrations.is_empty() && language == Language::Php {
if has_constructor && !needs_constructor {
for hook_name in &missing_registrations {
insertions.push(Insertion {
kind: InsertionKind::RegistrationStub,
code: generate_registration_stub(hook_name),
description: format!(
"Add {} registration in __construct()",
hook_name
),
});
}
} else {
let reg_lines: String = missing_registrations
.iter()
.map(|h| generate_registration_stub(h))
.collect::<Vec<_>>()
.join("\n");
let construct_code = format!(
"\n public function __construct() {{\n{}\n }}\n",
reg_lines
);
insertions.push(Insertion {
kind: InsertionKind::ConstructorWithRegistration,
code: construct_code,
description: format!(
"Add __construct() with {} registration(s)",
missing_registrations.len()
),
});
needs_constructor = false;
}
}
if needs_constructor {
let constructor_name = match language {
Language::Php => "__construct",
Language::Rust => "new",
Language::JavaScript | Language::TypeScript => "constructor",
Language::Unknown => "__construct",
};
if let Some(sig) = sig_map.get(constructor_name) {
insertions.push(Insertion {
kind: InsertionKind::MethodStub,
code: generate_method_stub(sig),
description: format!(
"Add {}() stub to match {} convention",
constructor_name, conv_report.name
),
});
} else {
let fallback_sig = generate_fallback_signature(constructor_name, &language);
insertions.push(Insertion {
kind: InsertionKind::MethodStub,
code: generate_method_stub(&fallback_sig),
description: format!(
"Add {}() stub to match {} convention (signature inferred)",
constructor_name, conv_report.name
),
});
}
}
for method_name in &missing_methods {
if let Some(sig) = sig_map.get(*method_name) {
insertions.push(Insertion {
kind: InsertionKind::MethodStub,
code: generate_method_stub(sig),
description: format!(
"Add {}() stub to match {} convention",
method_name, conv_report.name
),
});
} else {
let fallback_sig = generate_fallback_signature(method_name, &language);
insertions.push(Insertion {
kind: InsertionKind::MethodStub,
code: generate_method_stub(&fallback_sig),
description: format!(
"Add {}() stub to match {} convention (signature inferred)",
method_name, conv_report.name
),
});
}
}
if !insertions.is_empty() {
fixes.push(Fix {
file: outlier.file.clone(),
insertions,
applied: false,
});
}
}
}
let total_insertions: usize = fixes.iter().map(|f| f.insertions.len()).sum();
let files_modified = fixes.len();
FixResult {
fixes,
skipped,
total_insertions,
files_modified,
}
}
fn detect_naming_suffix(conforming: &[String]) -> Option<String> {
if conforming.len() < 2 {
return None;
}
let stems: Vec<String> = conforming
.iter()
.filter_map(|f| {
Path::new(f)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
})
.collect();
if stems.len() < 2 {
return None;
}
let mut suffix_counts: HashMap<String, usize> = HashMap::new();
for stem in &stems {
if let Some(suffix) = extract_class_suffix(stem) {
*suffix_counts.entry(suffix).or_insert(0) += 1;
}
}
let threshold = (stems.len() as f32 * 0.6).ceil() as usize;
suffix_counts
.into_iter()
.filter(|(_, count)| *count >= threshold)
.max_by_key(|(_, count)| *count)
.map(|(suffix, _)| suffix)
}
fn extract_class_suffix(name: &str) -> Option<String> {
let chars: Vec<char> = name.chars().collect();
let mut last_upper_start = None;
for (i, ch) in chars.iter().enumerate() {
if ch.is_uppercase() && i > 0 {
last_upper_start = Some(i);
}
}
last_upper_start.map(|i| chars[i..].iter().collect())
}
fn suffix_matches(file_stem: &str, suffix: &str) -> bool {
if file_stem.ends_with(suffix) {
return true;
}
let plural_suffix = pluralize(suffix);
if file_stem.ends_with(&plural_suffix) {
return true;
}
if let Some(singular) = singularize(suffix) {
if file_stem.ends_with(&singular) {
return true;
}
}
false
}
fn pluralize(word: &str) -> String {
if word.ends_with('y') && !word.ends_with("ey") && !word.ends_with("ay") && !word.ends_with("oy") {
format!("{}ies", &word[..word.len() - 1])
} else if word.ends_with('s') || word.ends_with('x') || word.ends_with("ch") || word.ends_with("sh") {
format!("{}es", word)
} else {
format!("{}s", word)
}
}
fn singularize(word: &str) -> Option<String> {
if word.ends_with("ies") && word.len() > 3 {
Some(format!("{}y", &word[..word.len() - 3]))
} else if word.ends_with("ses") || word.ends_with("xes") || word.ends_with("ches") || word.ends_with("shes") {
Some(word[..word.len() - 2].to_string())
} else if word.ends_with('s') && !word.ends_with("ss") && word.len() > 1 {
Some(word[..word.len() - 1].to_string())
} else {
None
}
}
fn generate_fallback_signature(method_name: &str, language: &Language) -> MethodSignature {
let signature = match language {
Language::Php => format!("public function {}()", method_name),
Language::Rust => format!("pub fn {}()", method_name),
Language::JavaScript | Language::TypeScript => format!("{}()", method_name),
Language::Unknown => format!("{}()", method_name),
};
MethodSignature {
name: method_name.to_string(),
signature,
language: language.clone(),
}
}
pub fn apply_fixes(fixes: &mut [Fix], root: &Path) -> usize {
let mut applied_count = 0;
for fix in fixes.iter_mut() {
let abs_path = root.join(&fix.file);
let content = match std::fs::read_to_string(&abs_path) {
Ok(c) => c,
Err(e) => {
log_status!("fix", "Failed to read {}: {}", fix.file, e);
continue;
}
};
let language = detect_language(&abs_path);
let modified = apply_insertions_to_content(&content, &fix.insertions, &language);
if modified != content {
match std::fs::write(&abs_path, &modified) {
Ok(_) => {
fix.applied = true;
applied_count += 1;
log_status!("fix", "Applied {} fix(es) to {}", fix.insertions.len(), fix.file);
}
Err(e) => {
log_status!("fix", "Failed to write {}: {}", fix.file, e);
}
}
}
}
applied_count
}
fn apply_insertions_to_content(
content: &str,
insertions: &[Insertion],
language: &Language,
) -> String {
let mut result = content.to_string();
let mut method_stubs = Vec::new();
let mut registration_stubs = Vec::new();
let mut constructor_stubs = Vec::new();
for insertion in insertions {
match insertion.kind {
InsertionKind::MethodStub => method_stubs.push(&insertion.code),
InsertionKind::RegistrationStub => registration_stubs.push(&insertion.code),
InsertionKind::ConstructorWithRegistration => constructor_stubs.push(&insertion.code),
}
}
if !registration_stubs.is_empty() {
result = insert_into_constructor(&result, ®istration_stubs, language);
}
if !constructor_stubs.is_empty() {
let combined: String = constructor_stubs.iter().map(|s| s.as_str()).collect::<Vec<_>>().join("");
result = insert_before_closing_brace(&result, &combined, language);
}
if !method_stubs.is_empty() {
let combined: String = method_stubs.iter().map(|s| s.as_str()).collect::<Vec<_>>().join("");
result = insert_before_closing_brace(&result, &combined, language);
}
result
}
fn insert_into_constructor(content: &str, stubs: &[&String], language: &Language) -> String {
let constructor_pattern = match language {
Language::Php => r"function\s+__construct\s*\([^)]*\)\s*\{",
Language::Rust => r"fn\s+new\s*\([^)]*\)\s*(?:->[^{]*)?\{",
Language::JavaScript | Language::TypeScript => r"constructor\s*\([^)]*\)\s*\{",
Language::Unknown => return content.to_string(),
};
let re = match Regex::new(constructor_pattern) {
Ok(r) => r,
Err(_) => return content.to_string(),
};
if let Some(m) = re.find(content) {
let insert_pos = m.end();
let insert_text: String = stubs.iter().map(|s| format!("\n{}", s)).collect();
let mut result = String::with_capacity(content.len() + insert_text.len());
result.push_str(&content[..insert_pos]);
result.push_str(&insert_text);
result.push_str(&content[insert_pos..]);
result
} else {
content.to_string()
}
}
fn insert_before_closing_brace(content: &str, code: &str, _language: &Language) -> String {
if let Some(last_brace) = content.rfind('}') {
let mut result = String::with_capacity(content.len() + code.len());
result.push_str(&content[..last_brace]);
result.push_str(code);
result.push_str(&content[last_brace..]);
result
} else {
format!("{}{}", content, code)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_php_signature_with_types() {
let content = r#"<?php
class MyAbility {
public function __construct(private Container $container) {}
public function execute(array $config): array {
return [];
}
public function registerAbility(): void {
// ...
}
protected function helper(): string {
return '';
}
}
"#;
let sigs = extract_php_signatures(content);
assert_eq!(sigs.len(), 4);
let execute = sigs.iter().find(|s| s.name == "execute").unwrap();
assert!(execute.signature.contains("array $config"));
assert!(execute.signature.contains(": array"));
let register = sigs.iter().find(|s| s.name == "registerAbility").unwrap();
assert!(register.signature.contains(": void"));
}
#[test]
fn extract_rust_signature_with_return_type() {
let content = r#"
pub struct Handler;
impl Handler {
pub fn new(config: Config) -> Self {
Self
}
pub fn run(&self, input: &str) -> Result<Output> {
todo!()
}
pub(crate) fn validate(&self) -> bool {
true
}
}
"#;
let sigs = extract_rust_signatures(content);
assert!(sigs.len() >= 2);
let run = sigs.iter().find(|s| s.name == "run").unwrap();
assert!(run.signature.contains("&self"));
assert!(run.signature.contains("Result<Output>"));
}
#[test]
fn generate_php_method_stub() {
let sig = MethodSignature {
name: "execute".to_string(),
signature: "public function execute(array $config): array".to_string(),
language: Language::Php,
};
let stub = generate_method_stub(&sig);
assert!(stub.contains("public function execute(array $config): array"));
assert!(stub.contains("throw new \\RuntimeException('Not implemented: execute')"));
}
#[test]
fn generate_rust_method_stub() {
let sig = MethodSignature {
name: "run".to_string(),
signature: "pub fn run(&self) -> Result<()>".to_string(),
language: Language::Rust,
};
let stub = generate_method_stub(&sig);
assert!(stub.contains("pub fn run(&self) -> Result<()>"));
assert!(stub.contains("todo!(\"run\")"));
}
#[test]
fn insert_method_before_closing_brace() {
let content = r#"<?php
class MyClass {
public function existing() {}
}
"#;
let stub = "\n public function newMethod() {\n // stub\n }\n";
let result = insert_before_closing_brace(content, stub, &Language::Php);
assert!(result.contains("newMethod"));
assert!(result.contains("existing"));
let new_pos = result.find("newMethod").unwrap();
let last_brace = result.rfind('}').unwrap();
assert!(new_pos < last_brace);
}
#[test]
fn insert_registration_into_constructor() {
let content = r#"<?php
class MyAbility {
public function __construct() {
$this->name = 'test';
}
public function execute() {}
}
"#;
let reg = " add_action('wp_abilities_api_init', [$this, 'abilities_api_init']);".to_string();
let result = insert_into_constructor(content, &[®], &Language::Php);
assert!(result.contains("add_action('wp_abilities_api_init'"));
let construct_pos = result.find("__construct").unwrap();
let reg_pos = result.find("add_action").unwrap();
assert!(reg_pos > construct_pos);
}
#[test]
fn constructor_with_registration_when_no_constructor() {
let content = r#"<?php
class MyAbility {
public function execute() {}
}
"#;
let insertions = vec![Insertion {
kind: InsertionKind::ConstructorWithRegistration,
code: "\n public function __construct() {\n add_action('wp_abilities_api_init', [$this, 'abilities_api_init']);\n }\n".to_string(),
description: "Add __construct with registration".to_string(),
}];
let result = apply_insertions_to_content(content, &insertions, &Language::Php);
assert!(result.contains("__construct"));
assert!(result.contains("add_action"));
assert!(result.contains("execute")); }
#[test]
fn fallback_signature_when_no_conforming_match() {
let sig = generate_fallback_signature("doSomething", &Language::Php);
assert_eq!(sig.signature, "public function doSomething()");
assert_eq!(sig.name, "doSomething");
}
#[test]
fn registration_stub_strips_wp_prefix() {
let stub = generate_registration_stub("wp_abilities_api_init");
assert!(stub.contains("'wp_abilities_api_init'"));
assert!(stub.contains("'abilities_api_init'"));
}
#[test]
fn registration_stub_strips_datamachine_prefix() {
let stub = generate_registration_stub("datamachine_chat_tools");
assert!(stub.contains("'datamachine_chat_tools'"));
assert!(stub.contains("'chat_tools'"));
}
#[test]
fn merged_constructor_with_method_and_registration() {
use super::super::conventions::{Deviation, DeviationKind, Outlier};
use super::super::checks::CheckStatus;
use super::super::{AuditSummary, CodeAuditResult, ConventionReport};
let dir = std::env::temp_dir().join("homeboy_fixer_merge_test");
let abilities = dir.join("abilities");
let _ = std::fs::create_dir_all(&abilities);
std::fs::write(
abilities.join("GoodAbility.php"),
r#"<?php
class GoodAbility {
public function __construct() {
add_action('wp_abilities_api_init', [$this, 'abilities_api_init']);
}
public function execute(array $config): array { return []; }
public function registerAbility(): void {}
}
"#,
).unwrap();
std::fs::write(
abilities.join("BadAbility.php"),
r#"<?php
class BadAbility {
public function execute(array $config): array { return []; }
}
"#,
).unwrap();
let audit_result = CodeAuditResult {
component_id: "test".to_string(),
source_path: dir.to_str().unwrap().to_string(),
summary: AuditSummary {
files_scanned: 2,
conventions_detected: 1,
outliers_found: 1,
alignment_score: 0.5,
},
conventions: vec![ConventionReport {
name: "Abilities".to_string(),
glob: "abilities/*".to_string(),
status: CheckStatus::Drift,
expected_methods: vec![
"__construct".to_string(),
"execute".to_string(),
"registerAbility".to_string(),
],
expected_registrations: vec!["wp_abilities_api_init".to_string()],
expected_interfaces: vec![],
expected_namespace: None,
expected_imports: vec![],
conforming: vec!["abilities/GoodAbility.php".to_string()],
outliers: vec![Outlier {
file: "abilities/BadAbility.php".to_string(),
deviations: vec![
Deviation {
kind: DeviationKind::MissingMethod,
description: "Missing method: __construct".to_string(),
suggestion: "Add __construct()".to_string(),
},
Deviation {
kind: DeviationKind::MissingMethod,
description: "Missing method: registerAbility".to_string(),
suggestion: "Add registerAbility()".to_string(),
},
Deviation {
kind: DeviationKind::MissingRegistration,
description: "Missing registration: wp_abilities_api_init".to_string(),
suggestion: "Add wp_abilities_api_init".to_string(),
},
],
}],
total_files: 2,
confidence: 0.5,
}],
findings: vec![],
directory_conventions: vec![],
};
let fix_result = generate_fixes(&audit_result, &dir);
assert_eq!(fix_result.fixes.len(), 1);
let fix = &fix_result.fixes[0];
assert_eq!(fix.insertions.len(), 2, "Expected 2 insertions, got: {:?}",
fix.insertions.iter().map(|i| &i.description).collect::<Vec<_>>());
let has_constructor_with_reg = fix.insertions.iter().any(|i|
matches!(i.kind, InsertionKind::ConstructorWithRegistration)
&& i.code.contains("add_action")
);
assert!(has_constructor_with_reg, "Should have constructor with registration");
let has_register_ability = fix.insertions.iter().any(|i|
matches!(i.kind, InsertionKind::MethodStub)
&& i.code.contains("registerAbility")
);
assert!(has_register_ability, "Should have registerAbility stub");
let has_bare_constructor = fix.insertions.iter().any(|i|
matches!(i.kind, InsertionKind::MethodStub)
&& i.code.contains("__construct")
);
assert!(!has_bare_constructor, "Should NOT have bare __construct stub");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn apply_fixes_writes_to_disk() {
let dir = std::env::temp_dir().join("homeboy_fixer_apply_test");
let _ = std::fs::create_dir_all(&dir);
std::fs::write(
dir.join("test.php"),
r#"<?php
class TestClass {
public function existing() {}
}
"#,
).unwrap();
let mut fixes = vec![Fix {
file: "test.php".to_string(),
insertions: vec![Insertion {
kind: InsertionKind::MethodStub,
code: "\n public function newMethod(): void {\n throw new \\RuntimeException('Not implemented: newMethod');\n }\n".to_string(),
description: "Add newMethod()".to_string(),
}],
applied: false,
}];
let applied = apply_fixes(&mut fixes, &dir);
assert_eq!(applied, 1);
assert!(fixes[0].applied);
let content = std::fs::read_to_string(dir.join("test.php")).unwrap();
assert!(content.contains("newMethod"));
assert!(content.contains("existing"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn detect_naming_suffix_from_ability_files() {
let conforming = vec![
"inc/Abilities/Flow/CreateFlowAbility.php".to_string(),
"inc/Abilities/Flow/UpdateFlowAbility.php".to_string(),
"inc/Abilities/Flow/DeleteFlowAbility.php".to_string(),
"inc/Abilities/Flow/GetFlowsAbility.php".to_string(),
];
let suffix = detect_naming_suffix(&conforming);
assert_eq!(suffix, Some("Ability".to_string()));
}
#[test]
fn detect_naming_suffix_returns_none_for_diverse_names() {
let conforming = vec![
"inc/Core/FileStorage.php".to_string(),
"inc/Core/AgentMemory.php".to_string(),
"inc/Core/Workspace.php".to_string(),
];
let suffix = detect_naming_suffix(&conforming);
assert!(suffix.is_none() || suffix == Some("Memory".to_string()).or(None));
}
#[test]
fn extract_class_suffix_pascal_case() {
assert_eq!(extract_class_suffix("CreateFlowAbility"), Some("Ability".to_string()));
assert_eq!(extract_class_suffix("FlowHelpers"), Some("Helpers".to_string()));
assert_eq!(extract_class_suffix("BlockSanitizer"), Some("Sanitizer".to_string()));
}
#[test]
fn suffix_matches_exact() {
assert!(suffix_matches("CreateFlowAbility", "Ability"));
assert!(suffix_matches("WebhookTriggerAbility", "Ability"));
assert!(!suffix_matches("FlowHelpers", "Ability"));
}
#[test]
fn suffix_matches_plural_tolerance() {
assert!(suffix_matches("GitHubAbilities", "Ability"));
assert!(suffix_matches("FetchAbilities", "Ability"));
assert!(suffix_matches("CreateFlowAbility", "Abilities"));
}
#[test]
fn suffix_matches_simple_plural() {
assert!(suffix_matches("AllTests", "Test"));
assert!(suffix_matches("SingleTest", "Tests"));
assert!(suffix_matches("AuthProviders", "Provider"));
}
#[test]
fn suffix_matches_rejects_unrelated() {
assert!(!suffix_matches("FlowHelpers", "Ability"));
assert!(!suffix_matches("BlockSanitizer", "Ability"));
assert!(!suffix_matches("EngineHelpers", "Tool"));
}
#[test]
fn pluralize_y_ending() {
assert_eq!(pluralize("Ability"), "Abilities");
assert_eq!(pluralize("Entity"), "Entities");
}
#[test]
fn pluralize_regular() {
assert_eq!(pluralize("Test"), "Tests");
assert_eq!(pluralize("Provider"), "Providers");
assert_eq!(pluralize("Tool"), "Tools");
}
#[test]
fn singularize_ies_ending() {
assert_eq!(singularize("Abilities"), Some("Ability".to_string()));
assert_eq!(singularize("Entities"), Some("Entity".to_string()));
}
#[test]
fn singularize_regular_s() {
assert_eq!(singularize("Tests"), Some("Test".to_string()));
assert_eq!(singularize("Providers"), Some("Provider".to_string()));
}
#[test]
fn skip_helper_files_in_ability_directory() {
use super::super::conventions::{Deviation, DeviationKind, Outlier};
use super::super::checks::CheckStatus;
use super::super::{AuditSummary, CodeAuditResult, ConventionReport};
let dir = std::env::temp_dir().join("homeboy_fixer_skip_helper_test");
let abilities = dir.join("abilities");
let _ = std::fs::create_dir_all(&abilities);
for name in &["CreateFlowAbility", "UpdateFlowAbility", "DeleteFlowAbility"] {
std::fs::write(
abilities.join(format!("{}.php", name)),
format!(r#"<?php
class {} {{
public function __construct() {{
add_action('wp_abilities_api_init', [$this, 'registerAbility']);
}}
public function execute(array $config): array {{ return []; }}
public function registerAbility(): void {{}}
}}
"#, name),
).unwrap();
}
std::fs::write(
abilities.join("FlowHelpers.php"),
"<?php\nclass FlowHelpers {\n public function formatFlow() {}\n}\n",
).unwrap();
let audit_result = CodeAuditResult {
component_id: "test".to_string(),
source_path: dir.to_str().unwrap().to_string(),
summary: AuditSummary {
files_scanned: 4,
conventions_detected: 1,
outliers_found: 1,
alignment_score: 0.75,
},
conventions: vec![ConventionReport {
name: "Flow".to_string(),
glob: "abilities/*".to_string(),
status: CheckStatus::Drift,
expected_methods: vec![
"__construct".to_string(),
"execute".to_string(),
"registerAbility".to_string(),
],
expected_registrations: vec!["wp_abilities_api_init".to_string()],
expected_interfaces: vec![],
expected_namespace: None,
expected_imports: vec![],
conforming: vec![
"abilities/CreateFlowAbility.php".to_string(),
"abilities/UpdateFlowAbility.php".to_string(),
"abilities/DeleteFlowAbility.php".to_string(),
],
outliers: vec![Outlier {
file: "abilities/FlowHelpers.php".to_string(),
deviations: vec![
Deviation {
kind: DeviationKind::MissingMethod,
description: "Missing method: execute".to_string(),
suggestion: "Add execute()".to_string(),
},
Deviation {
kind: DeviationKind::MissingMethod,
description: "Missing method: registerAbility".to_string(),
suggestion: "Add registerAbility()".to_string(),
},
Deviation {
kind: DeviationKind::MissingRegistration,
description: "Missing registration: wp_abilities_api_init".to_string(),
suggestion: "Add wp_abilities_api_init".to_string(),
},
],
}],
total_files: 4,
confidence: 0.75,
}],
findings: vec![],
directory_conventions: vec![],
};
let fix_result = generate_fixes(&audit_result, &dir);
assert!(fix_result.fixes.is_empty(), "Should not generate fixes for FlowHelpers");
assert_eq!(fix_result.skipped.len(), 1);
assert!(fix_result.skipped[0].file.contains("FlowHelpers"));
assert!(fix_result.skipped[0].reason.contains("utility/helper"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn skip_fragmented_conventions() {
use super::super::conventions::{Deviation, DeviationKind, Outlier};
use super::super::checks::CheckStatus;
use super::super::{AuditSummary, CodeAuditResult, ConventionReport};
let dir = std::env::temp_dir().join("homeboy_fixer_skip_frag_test");
let _ = std::fs::create_dir_all(&dir);
let audit_result = CodeAuditResult {
component_id: "test".to_string(),
source_path: dir.to_str().unwrap().to_string(),
summary: AuditSummary {
files_scanned: 3,
conventions_detected: 1,
outliers_found: 2,
alignment_score: 0.33,
},
conventions: vec![ConventionReport {
name: "Jobs".to_string(),
glob: "jobs/*".to_string(),
status: CheckStatus::Fragmented,
expected_methods: vec!["get_job".to_string()],
expected_registrations: vec![],
expected_interfaces: vec![],
expected_namespace: None,
expected_imports: vec![],
conforming: vec!["jobs/Jobs.php".to_string()],
outliers: vec![
Outlier {
file: "jobs/JobsStatus.php".to_string(),
deviations: vec![Deviation {
kind: DeviationKind::MissingMethod,
description: "Missing method: get_job".to_string(),
suggestion: "Add get_job()".to_string(),
}],
},
Outlier {
file: "jobs/JobsOps.php".to_string(),
deviations: vec![Deviation {
kind: DeviationKind::MissingMethod,
description: "Missing method: get_job".to_string(),
suggestion: "Add get_job()".to_string(),
}],
},
],
total_files: 3,
confidence: 0.33,
}],
findings: vec![],
directory_conventions: vec![],
};
let fix_result = generate_fixes(&audit_result, &dir);
assert!(fix_result.fixes.is_empty());
assert_eq!(fix_result.skipped.len(), 2);
assert!(fix_result.skipped[0].reason.contains("confidence too low"));
let _ = std::fs::remove_dir_all(&dir);
}
}