use std::collections::HashMap;
use std::path::Path;
use serde_json::{json, Value};
use super::helpers::{count_lines_in_span, extract_symbols_with_language, log_execution_error};
#[allow(
clippy::too_many_arguments,
reason = "CLI handler aggregates clap-parsed flags"
)]
pub(crate) fn execute_delete(
file_path: &Path,
symbol_name: &str,
kind: Option<splice::cli::SymbolKind>,
analyzer: Option<splice::cli::AnalyzerMode>,
analyzer_binary: Option<std::path::PathBuf>,
language: Option<splice::cli::Language>,
context_before: usize,
context_after: usize,
context: usize,
create_backup: bool,
relationships: bool,
dry_run: bool,
unified: usize,
operation_id: Option<String>,
metadata: Option<String>,
snapshot_before: bool,
json_output: bool,
strict: bool,
skip_pre_verify: bool,
) -> Result<splice::cli::CliSuccessPayload, splice::SpliceError> {
use ropey::Rope;
use splice::execution::log;
use splice::format_colored_diff;
use splice::format_diff_summary;
use splice::format_unified_diff;
use splice::graph::CodeGraph;
use splice::patch::apply_patch_with_validation;
use splice::resolve::references::find_references;
use splice::should_use_color;
use splice::symbol::{Language as SymbolLanguage, Symbol};
use splice::validate::AnalyzerMode as ValidateAnalyzerMode;
let (ctx_before, ctx_after) =
splice::resolve_context_counts(context_before, context_after, context);
if snapshot_before {
eprintln!("Warning: --snapshot-before is not yet supported for delete operations");
}
let start = std::time::Instant::now();
let command_line = std::env::args().collect::<Vec<_>>().join(" ");
let symbol_lang = language
.map(|l| l.to_symbol_language())
.or_else(|| SymbolLanguage::from_path(file_path));
let symbol_lang = symbol_lang.ok_or_else(|| splice::SpliceError::Parse {
file: file_path.to_path_buf(),
message: "Cannot detect language - unknown file extension".to_string(),
})?;
let source = std::fs::read(file_path).map_err(|source| splice::SpliceError::Io {
path: file_path.to_path_buf(),
source,
})?;
let symbols = extract_symbols_with_language(file_path, &source, symbol_lang)?;
let graph_db_path = file_path
.parent()
.ok_or_else(|| {
splice::SpliceError::Other(format!("File path has no parent: {}", file_path.display()))
})?
.join(".splice_graph.db");
let mut code_graph = CodeGraph::open(&graph_db_path)?;
for symbol in &symbols {
code_graph.store_symbol_with_file_and_language(
file_path,
symbol.name(),
symbol.kind(),
symbol.language(),
symbol.byte_start(),
symbol.byte_end(),
symbol.line_start(),
symbol.line_end(),
symbol.col_start(),
symbol.col_end(),
)?;
}
let _kind_str = kind.map(|k| match k {
splice::cli::SymbolKind::Function => "function",
splice::cli::SymbolKind::Method => "method",
splice::cli::SymbolKind::Class => "class",
splice::cli::SymbolKind::Struct => "struct",
splice::cli::SymbolKind::Interface => "interface",
splice::cli::SymbolKind::Enum => "enum",
splice::cli::SymbolKind::Trait => "trait",
splice::cli::SymbolKind::Impl => "impl",
splice::cli::SymbolKind::Module => "module",
splice::cli::SymbolKind::Variable => "variable",
splice::cli::SymbolKind::Constructor => "constructor",
splice::cli::SymbolKind::TypeAlias => "type_alias",
});
let ref_set = find_references(&code_graph, file_path, symbol_name, None)?;
let workspace_dir = file_path.parent().ok_or_else(|| {
splice::SpliceError::Other("Cannot determine workspace directory".to_string())
})?;
let analyzer_mode = match analyzer {
Some(splice::cli::AnalyzerMode::Off) => ValidateAnalyzerMode::Off,
Some(splice::cli::AnalyzerMode::Os) => ValidateAnalyzerMode::Path,
Some(splice::cli::AnalyzerMode::Path) => {
if let Some(binary) = analyzer_binary {
ValidateAnalyzerMode::Explicit(binary.to_string_lossy().to_string())
} else {
ValidateAnalyzerMode::Path
}
}
None => ValidateAnalyzerMode::Off,
};
let mut refs_by_file: HashMap<String, Vec<&splice::resolve::references::Reference>> =
HashMap::new();
for r in &ref_set.references {
refs_by_file.entry(r.file_path.clone()).or_default().push(r);
}
for refs in refs_by_file.values_mut() {
refs.sort_by_key(|r| std::cmp::Reverse(r.byte_start));
}
if dry_run {
let replaced_content =
std::fs::read_to_string(file_path).map_err(|source| splice::SpliceError::Io {
path: file_path.to_path_buf(),
source,
})?;
let mut rope = Rope::from_str(&replaced_content);
let def = &ref_set.definition;
let start_char = rope.byte_to_char(def.byte_start);
let end_char = rope.byte_to_char(def.byte_end);
rope.remove(start_char..end_char);
let after_content = rope.to_string();
let lines_removed = if def.byte_end > def.byte_start {
replaced_content[def.byte_start..def.byte_end]
.lines()
.count()
} else {
0
};
let summary_header = format_diff_summary(1, 0, lines_removed);
if !summary_header.is_empty() {
println!("{}", summary_header);
}
println!();
let use_color = !json_output && should_use_color();
let diff_output = if use_color {
format_colored_diff(&replaced_content, &after_content, true)
} else {
format_unified_diff(
&replaced_content,
&after_content,
&file_path.to_string_lossy(),
unified,
)
};
if !diff_output.is_empty() {
print!("{}", diff_output);
}
let message = format!("Previewed deletion of '{}' (dry-run)", symbol_name,);
let duration_ms = start.elapsed().as_millis() as i64;
let parameters = serde_json::json!({
"file": file_path.to_string_lossy(),
"symbol": symbol_name,
"kind": _kind_str,
"create_backup": false,
"dry_run": true,
});
if let Err(e) = log::record_execution_with_params(
&splice::output::OperationResult::with_execution_id(
"delete".to_string(),
operation_id.clone(),
)
.success(message.clone()),
duration_ms,
Some(command_line),
parameters,
) {
log_execution_error("delete (dry-run)", &e);
}
let has_changes = lines_removed > 0;
let mut payload = splice::cli::CliSuccessPayload::message_only(message).already_emitted();
if has_changes {
payload = payload.with_pending_changes();
}
return Ok(payload);
}
let backup_manifest_path = if create_backup {
use splice::patch::BackupWriter;
let workspace_root = splice::workspace::find_workspace_root(file_path)?;
let mut backup_writer = BackupWriter::new(&workspace_root, operation_id.clone())?;
backup_writer.backup_file(file_path)?;
for file_path_str in refs_by_file.keys() {
let path = Path::new(file_path_str);
if path != file_path {
backup_writer.backup_file(path)?;
}
}
Some(backup_writer.finalize()?)
} else {
None
};
let mut deleted_count = 0;
let mut files_modified = Vec::new();
for (file_path_str, refs) in refs_by_file {
let path = Path::new(&file_path_str);
let file_lang = SymbolLanguage::from_path(path).unwrap_or(symbol_lang);
for r in refs {
apply_patch_with_validation(
path,
r.byte_start,
r.byte_end,
"", workspace_dir,
file_lang,
analyzer_mode.clone(),
strict,
skip_pre_verify,
)?;
deleted_count += 1;
}
files_modified.push(file_path_str);
}
let def = &ref_set.definition;
apply_patch_with_validation(
file_path,
def.byte_start,
def.byte_end,
"", workspace_dir,
symbol_lang,
analyzer_mode.clone(),
strict,
skip_pre_verify,
)?;
deleted_count += 1;
let def_file_path = file_path.to_str().unwrap_or("").to_string();
if !files_modified.contains(&def_file_path) {
files_modified.push(def_file_path);
}
let base_message = if ref_set.has_glob_ambiguity {
format!(
"Deleted '{}' ({} references + definition) across {} file(s). WARNING: glob imports detected - some references may have been missed.",
symbol_name,
deleted_count - 1,
files_modified.len()
)
} else {
format!(
"Deleted '{}' ({} references + definition) across {} file(s).",
symbol_name,
deleted_count - 1,
files_modified.len()
)
};
let mut span_ids: Vec<serde_json::Value> = Vec::new();
for r in &ref_set.references {
span_ids.push(json!({
"file": r.file_path,
"byte_start": r.byte_start,
"byte_end": r.byte_end,
}));
}
span_ids.push(json!({
"file": file_path.to_string_lossy(),
"byte_start": def.byte_start,
"byte_end": def.byte_end,
}));
let mut response_data = serde_json::Map::new();
if let Some(manifest_path) = backup_manifest_path {
response_data.insert(
"backup_manifest".to_string(),
json!(manifest_path.to_string_lossy()),
);
}
if let Some(ref op_id) = operation_id {
response_data.insert("operation_id".to_string(), json!(op_id));
}
if let Some(meta) = metadata {
if let Ok(parsed) = serde_json::from_str::<Value>(&meta) {
response_data.insert("metadata".to_string(), parsed);
} else {
response_data.insert("metadata".to_string(), json!(meta));
}
}
response_data.insert("span_ids".to_string(), json!(span_ids));
response_data.insert("files_modified".to_string(), json!(files_modified));
let duration_ms = start.elapsed().as_millis() as i64;
let parameters = serde_json::json!({
"file": file_path.to_string_lossy(),
"symbol": symbol_name,
"kind": _kind_str,
"create_backup": create_backup,
});
if let Err(e) = log::record_execution_with_params(
&splice::output::OperationResult::with_execution_id(
"delete".to_string(),
operation_id.clone(),
)
.success(base_message.clone()),
duration_ms,
Some(command_line.clone()),
parameters,
) {
log_execution_error("delete", &e);
}
if json_output {
use splice::action::SuggestedAction;
use splice::action::{ActionType, Confidence};
use splice::checksum;
use splice::context;
use splice::hints::{derive_tool_hints, ToolHintOperation};
use splice::ingest::semantic_kind::SemanticKind;
use splice::ingest::{detect as ingest_detect, dispatch};
use splice::output::{DeleteResult, OperationData, OperationResult, SpanResult};
use splice::resolve::resolve_symbol;
use splice::symbol::AnySymbol;
use std::path::Path;
let resolved_def = resolve_symbol(&code_graph, Some(file_path), _kind_str, symbol_name)?;
let detected_language = ingest_detect::detect_language(file_path);
let file_contents = std::fs::read(file_path).unwrap_or_default();
let mut spans: Vec<SpanResult> = Vec::new();
let mut def_span = SpanResult::from(resolved_def.clone());
if let Ok(ctx) = context::extract_context_asymmetric(
file_path,
def.byte_start,
def.byte_end,
ctx_before,
ctx_after,
) {
def_span = def_span.with_context(ctx);
}
if let Some(lang) = detected_language {
let sem_kind_str = if let Ok(symbols) =
dispatch::extract_symbols(file_path, &file_contents)
{
symbols
.iter()
.find(|s| s.byte_start() == def.byte_start && s.byte_end() == def.byte_end)
.map(|s| {
match s {
AnySymbol::Rust(rust_sym) => match rust_sym.kind {
splice::ingest::rust::RustSymbolKind::Function => "function",
splice::ingest::rust::RustSymbolKind::Struct => "type",
splice::ingest::rust::RustSymbolKind::Enum => "enum",
splice::ingest::rust::RustSymbolKind::Trait => "trait",
splice::ingest::rust::RustSymbolKind::Impl => "trait",
splice::ingest::rust::RustSymbolKind::Module => "module",
splice::ingest::rust::RustSymbolKind::TypeAlias => "type_alias",
_ => "unknown",
},
AnySymbol::Python(py_sym) => match py_sym.kind {
splice::ingest::python::PythonSymbolKind::Function => "function",
splice::ingest::python::PythonSymbolKind::Class => "type",
splice::ingest::python::PythonSymbolKind::Method => "function",
_ => "unknown",
},
AnySymbol::Java(java_sym) => match java_sym.kind {
splice::ingest::java::JavaSymbolKind::Class => "type",
splice::ingest::java::JavaSymbolKind::Method => "function",
splice::ingest::java::JavaSymbolKind::Interface => "trait",
splice::ingest::java::JavaSymbolKind::Enum => "enum",
_ => "unknown",
},
AnySymbol::JavaScript(js_sym) => match js_sym.kind {
splice::ingest::javascript::JavaScriptSymbolKind::Function => {
"function"
}
splice::ingest::javascript::JavaScriptSymbolKind::Class => "type",
splice::ingest::javascript::JavaScriptSymbolKind::Method => {
"function"
}
_ => "unknown",
},
AnySymbol::TypeScript(ts_sym) => match ts_sym.kind {
splice::ingest::typescript::TypeScriptSymbolKind::Function => {
"function"
}
splice::ingest::typescript::TypeScriptSymbolKind::Class => "type",
splice::ingest::typescript::TypeScriptSymbolKind::Method => {
"function"
}
splice::ingest::typescript::TypeScriptSymbolKind::Interface => {
"trait"
}
_ => "unknown",
},
AnySymbol::Cpp(cpp_sym) => match cpp_sym.kind {
splice::ingest::cpp::CppSymbolKind::Class => "type",
splice::ingest::cpp::CppSymbolKind::Struct => "type",
splice::ingest::cpp::CppSymbolKind::Function => "function",
splice::ingest::cpp::CppSymbolKind::Method => "function",
_ => "unknown",
},
}
})
.unwrap_or("unknown")
} else {
"unknown"
};
def_span = def_span.with_semantic_info(sem_kind_str, lang.as_str());
let sem_kind = match sem_kind_str {
"function" => SemanticKind::Function,
"type" => SemanticKind::Type,
"trait" => SemanticKind::Trait,
"enum" => SemanticKind::Enum,
"module" => SemanticKind::Module,
"type_alias" => SemanticKind::TypeAlias,
"constant" => SemanticKind::Constant,
_ => SemanticKind::Unknown,
};
let is_public = matches!(
sem_kind,
SemanticKind::Function
| SemanticKind::Type
| SemanticKind::Trait
| SemanticKind::Enum
);
let hints = derive_tool_hints(sem_kind, is_public, ToolHintOperation::DeleteBody);
def_span = def_span.with_tool_hints(hints);
let has_callers = !ref_set.references.is_empty();
let confidence = if has_callers {
Confidence::Medium
} else {
Confidence::High
};
let reason = if has_callers {
format!(
"Delete symbol '{}' ({}) at {} - has {} callers, may break dependencies",
symbol_name,
sem_kind_str,
file_path.to_string_lossy(),
ref_set.references.len()
)
} else {
format!(
"Delete symbol '{}' ({}) at {} - safe to delete, no callers",
symbol_name,
sem_kind_str,
file_path.to_string_lossy()
)
};
let action = SuggestedAction {
action_type: ActionType::Delete,
confidence,
reason,
params: {
let mut p = std::collections::HashMap::new();
p.insert(
"remove_references".to_string(),
serde_json::Value::Bool(true),
);
Some(p)
},
};
def_span = def_span.with_suggested_action(action);
}
if let Ok(cs) = checksum::checksum_span(file_path, def.byte_start, def.byte_end) {
def_span = def_span.with_checksum_before(cs.value);
}
if let Ok(file_cs) = checksum::checksum_file(file_path) {
def_span = def_span.with_file_checksum_before(file_cs.value);
}
if relationships {
use splice::relationships::{
get_callees, get_callers, get_exports, get_imports, RelationshipCache,
Relationships,
};
use sqlitegraph::NodeId;
let mut cache = RelationshipCache::new();
let node_id = NodeId::from(resolved_def.node_id.as_i64());
let callers = get_callers(&code_graph, node_id, &mut cache).unwrap_or_default();
let callees = get_callees(&code_graph, node_id, &mut cache).unwrap_or_default();
let imports = get_imports(&code_graph, file_path, &mut cache).unwrap_or_default();
let exports = get_exports(&code_graph, file_path, &mut cache).unwrap_or_default();
let rels = Relationships {
callers,
callees,
imports,
exports,
cycle_detected: false,
error_code: None,
};
def_span = def_span.with_relationships(rels);
}
spans.push(def_span);
for r in &ref_set.references {
let ref_path = Path::new(&r.file_path);
let mut ref_span =
SpanResult::from_byte_span(r.file_path.clone(), r.byte_start, r.byte_end);
if let Ok(ctx) = context::extract_context_asymmetric(
ref_path,
r.byte_start,
r.byte_end,
ctx_before,
ctx_after,
) {
ref_span = ref_span.with_context(ctx);
}
if let Some(ref_lang) = ingest_detect::detect_language(ref_path) {
ref_span = ref_span.with_semantic_info("reference", ref_lang.as_str());
}
if let Ok(cs) = checksum::checksum_span(ref_path, r.byte_start, r.byte_end) {
ref_span = ref_span.with_checksum_before(cs.value);
}
if let Ok(file_cs) = checksum::checksum_file(ref_path) {
ref_span = ref_span.with_file_checksum_before(file_cs.value);
}
spans.push(ref_span);
}
spans.sort();
let total_bytes_removed: usize = ref_set
.references
.iter()
.map(|r| r.byte_end - r.byte_start)
.sum::<usize>()
+ (def.byte_end - def.byte_start);
let total_lines_removed: usize = {
let def_lines = if def.byte_end > def.byte_start {
count_lines_in_span(file_path, def.byte_start, def.byte_end)
} else {
0
};
let ref_lines: usize = ref_set
.references
.iter()
.map(|r| {
if r.byte_end > r.byte_start {
count_lines_in_span(Path::new(&r.file_path), r.byte_start, r.byte_end)
} else {
0
}
})
.sum();
def_lines + ref_lines
};
let file_checksum_before = checksum::checksum_file(file_path)
.map(|cs| cs.value)
.unwrap_or_else(|_| "checksum-failed".to_string());
let mut span_checksums: Vec<String> = Vec::new();
if let Ok(cs) = checksum::checksum_span(file_path, def.byte_start, def.byte_end) {
span_checksums.push(cs.value);
}
for r in &ref_set.references {
if let Ok(cs) =
checksum::checksum_span(Path::new(&r.file_path), r.byte_start, r.byte_end)
{
span_checksums.push(cs.value);
}
}
let delete_result = DeleteResult {
file: file_path.to_string_lossy().to_string(),
symbol: symbol_name.to_string(),
kind: _kind_str.unwrap_or("unknown").to_string(),
spans,
bytes_removed: total_bytes_removed,
lines_removed: total_lines_removed,
references_removed: deleted_count - 1,
file_checksum_before,
span_checksums,
};
let result = OperationResult::with_execution_id("delete".to_string(), operation_id.clone())
.success(base_message.clone())
.with_result(OperationData::Delete(delete_result));
let duration_ms = start.elapsed().as_millis() as i64;
let parameters = serde_json::json!({
"file": file_path.to_string_lossy(),
"symbol": symbol_name,
"kind": _kind_str,
"create_backup": create_backup,
});
if let Err(e) = log::record_execution_with_params(
&result,
duration_ms,
Some(command_line.clone()),
parameters,
) {
log_execution_error("delete", &e);
}
println!(
"{}",
serde_json::to_string_pretty(&result)
.expect("invariant: serde_json serialization never fails on serializable types")
);
return Ok(
splice::cli::CliSuccessPayload::message_only("OK".to_string()).already_emitted(),
);
}
Ok(splice::cli::CliSuccessPayload::with_data(
base_message,
serde_json::Value::Object(response_data),
))
}