use std::path::Path;
use serde::Serialize;
use splice::cli::OutputFormat;
#[allow(
clippy::too_many_arguments,
reason = "CLI handler aggregates clap-parsed flags"
)]
pub(crate) fn execute_suggest(
fn_name: &str,
desc: &str,
db: Option<&Path>,
output: OutputFormat,
_json_output: bool,
) -> Result<splice::cli::CliSuccessPayload, splice::SpliceError> {
let db_path = if let Some(db) = db {
db.to_path_buf()
} else {
splice::graph::discover_db_path(None)?.path
};
if !db_path.exists() {
return Err(splice::SpliceError::IoContext {
context: format!("Database not found: {}", db_path.display()),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "database not found"),
});
}
let mut magellan = splice::graph::MagellanIntegration::open(&db_path)?;
let pattern = format!("{}*", fn_name);
let matching_symbols = magellan.find_symbol_by_name(&pattern, false)?;
let keywords = extract_keywords(desc);
let caller_candidates = find_caller_candidates(&mut magellan, &keywords);
let scaffold = generate_scaffold(fn_name, desc, &matching_symbols, &caller_candidates);
match output {
splice::cli::OutputFormat::Json => {
let json_data = serde_json::json!({
"fn_name": fn_name,
"signature": scaffold.signature,
"imports_needed": scaffold.imports_needed,
"similar_existing": scaffold.similar_existing,
"callers_found": scaffold.callers_found,
"body_scaffold": scaffold.body_scaffold,
});
Ok(splice::cli::CliSuccessPayload {
status: "success",
message: format!("Generated scaffold for {}", fn_name),
data: Some(json_data),
already_emitted: false,
has_pending_changes: false,
})
}
splice::cli::OutputFormat::Pretty => {
let pretty_output = format_pretty_scaffold(&scaffold);
Ok(splice::cli::CliSuccessPayload {
status: "success",
message: format!("Generated scaffold for {}", fn_name),
data: Some(serde_json::json!({
"scaffold": pretty_output,
"details": scaffold
})),
already_emitted: false,
has_pending_changes: false,
})
}
splice::cli::OutputFormat::Human => {
let human_output = format_human_scaffold(&scaffold);
Ok(splice::cli::CliSuccessPayload {
status: "success",
message: format!("Generated scaffold for {}", fn_name),
data: Some(serde_json::json!({
"output": human_output
})),
already_emitted: false,
has_pending_changes: false,
})
}
}
}
#[derive(Debug, Clone, Serialize)]
struct Scaffold {
signature: String,
imports_needed: Vec<String>,
similar_existing: Vec<String>,
callers_found: Vec<String>,
body_scaffold: String,
}
fn extract_keywords(desc: &str) -> Vec<String> {
let common_words = [
"a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "by",
"from", "as", "is", "was", "are", "were", "be", "been", "being", "have", "has", "had",
"do", "does", "did", "will", "would", "should", "could", "may", "might", "can", "must",
];
desc.split_whitespace()
.map(|word| word.to_lowercase())
.filter(|word| !common_words.contains(&word.as_str()))
.collect()
}
fn find_caller_candidates(
magellan: &mut splice::graph::MagellanIntegration,
keywords: &[String],
) -> Vec<String> {
let mut candidates = Vec::new();
for keyword in keywords {
if let Ok(symbols) = magellan.find_symbol_by_name(keyword, false) {
for symbol in symbols {
let location = format!("{}:{}", symbol.file_path, symbol.byte_start);
if !candidates.contains(&location) {
candidates.push(location);
}
}
}
}
candidates
}
fn generate_scaffold(
fn_name: &str,
desc: &str,
matching_symbols: &[splice::graph::magellan_integration::SymbolInfo],
caller_candidates: &[String],
) -> Scaffold {
let imports_needed = extract_imports_from_symbols(matching_symbols);
let similar_existing: Vec<String> = matching_symbols
.iter()
.take(5) .map(|sym| sym.name.clone())
.collect();
let signature = infer_signature(fn_name, desc, &imports_needed);
let body_scaffold = format!("// TODO: Implement based on description: {}", desc);
Scaffold {
signature,
imports_needed,
similar_existing,
callers_found: caller_candidates.to_vec(),
body_scaffold,
}
}
fn extract_imports_from_symbols(
symbols: &[splice::graph::magellan_integration::SymbolInfo],
) -> Vec<String> {
let mut imports = std::collections::HashSet::new();
for symbol in symbols {
let module = &symbol.file_path;
if let Some(import_path) = module.strip_prefix("/home/feanor/Projects/splice/") {
let rust_path = import_path.replace(".rs", "").replace("/", "::");
imports.insert(rust_path);
}
}
imports.into_iter().collect()
}
fn infer_signature(fn_name: &str, desc: &str, _imports: &[String]) -> String {
let returns_result = desc.to_lowercase().contains("parse")
|| desc.to_lowercase().contains("read")
|| desc.to_lowercase().contains("load");
if returns_result {
format!("fn {}() -> Result<(), Error>", fn_name)
} else {
format!("fn {}()", fn_name)
}
}
fn format_human_scaffold(scaffold: &Scaffold) -> String {
let mut output = String::new();
output.push_str(&format!("{}\n", scaffold.signature));
output.push('\n');
if !scaffold.imports_needed.is_empty() {
output.push_str("Imports needed:\n");
for import in &scaffold.imports_needed {
output.push_str(&format!(" use {};\n", import));
}
output.push('\n');
}
if !scaffold.similar_existing.is_empty() {
output.push_str("Similar existing functions:\n");
for sim in &scaffold.similar_existing {
output.push_str(&format!(" - {}\n", sim));
}
output.push('\n');
}
if !scaffold.callers_found.is_empty() {
output.push_str("Potential callers:\n");
for caller in &scaffold.callers_found {
output.push_str(&format!(" - {}\n", caller));
}
output.push('\n');
}
output.push_str("Body scaffold:\n");
output.push_str(&scaffold.body_scaffold);
output
}
fn format_pretty_scaffold(scaffold: &Scaffold) -> String {
format_human_scaffold(scaffold)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_keywords() {
let keywords = extract_keywords("Parse configuration file and return AppConfig");
assert!(keywords.contains(&"parse".to_string()));
assert!(keywords.contains(&"configuration".to_string()));
assert!(keywords.contains(&"file".to_string()));
assert!(keywords.contains(&"return".to_string()));
assert!(keywords.contains(&"appconfig".to_string()));
}
#[test]
fn test_extract_keywords_filters_common_words() {
let keywords = extract_keywords("The function reads the file and returns the result");
assert!(!keywords.contains(&"the".to_string()));
assert!(!keywords.contains(&"and".to_string()));
assert!(keywords.contains(&"function".to_string()));
assert!(keywords.contains(&"reads".to_string()));
}
#[test]
fn test_infer_signature_with_result() {
let signature = infer_signature("parse_config", "Parse TOML config file", &[]);
assert!(signature.contains("Result"));
}
#[test]
fn test_infer_signature_simple() {
let signature = infer_signature("hello", "Say hello world", &[]);
assert!(!signature.contains("Result"));
}
#[test]
fn test_format_human_scaffold() {
let scaffold = Scaffold {
signature: "fn test() -> Result<(), Error>".to_string(),
imports_needed: vec!["std::fs".to_string()],
similar_existing: vec!["test_v1".to_string()],
callers_found: vec!["main.rs:42".to_string()],
body_scaffold: "// TODO: Implement".to_string(),
};
let output = format_human_scaffold(&scaffold);
assert!(output.contains("fn test()"));
assert!(output.contains("Imports needed"));
assert!(output.contains("Similar existing"));
assert!(output.contains("Potential callers"));
assert!(output.contains("Body scaffold"));
}
}