use anyhow::Result;
use magellan::output::command::{SliceResponse, SliceStats, Span, SymbolMatch};
use magellan::output::{output_json, JsonResponse, OutputFormat};
use magellan::CodeGraph;
use std::path::{Path, PathBuf};
struct ResolvedTarget {
pub symbol_id: String,
pub fqn: String,
pub file_path: String,
pub kind: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CliSliceDirection {
Backward,
Forward,
}
impl CliSliceDirection {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"backward" => Some(CliSliceDirection::Backward),
"forward" => Some(CliSliceDirection::Forward),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
CliSliceDirection::Backward => "backward",
CliSliceDirection::Forward => "forward",
}
}
}
fn resolve_target(graph: &mut CodeGraph, _db_path: &Path, target: &str) -> Result<ResolvedTarget> {
resolve_target_sqlite(graph, target)
}
fn resolve_target_sqlite(graph: &mut CodeGraph, target: &str) -> Result<ResolvedTarget> {
use magellan::graph::query;
use magellan::ingest::SymbolFact;
if target.len() == 64 || target.len() == 32 {
if let Ok(Some(_symbol)) = query::find_by_symbol_id(graph, target) {
if let Ok(entity_id) = graph.resolve_symbol_entity(target) {
if let Ok(info) = graph.symbol_by_entity_id(entity_id) {
return Ok(ResolvedTarget {
symbol_id: target.to_string(),
fqn: info.fqn.unwrap_or_else(|| target.to_string()),
file_path: info.file_path,
kind: info.kind,
});
}
}
}
}
match graph.resolve_symbol_entity(target) {
Ok(entity_id) => {
if let Ok(info) = graph.symbol_by_entity_id(entity_id) {
return Ok(ResolvedTarget {
symbol_id: info.symbol_id.unwrap_or_default(),
fqn: info.fqn.unwrap_or_else(|| target.to_string()),
file_path: info.file_path,
kind: info.kind,
});
}
}
Err(_) => {
}
}
let file_nodes = graph.all_file_nodes()?;
let mut matches: Vec<(i64, SymbolFact, Option<String>)> = Vec::new();
for file_path in file_nodes.keys() {
let entries = query::symbol_nodes_in_file_with_ids(graph, file_path)?;
for (node_id, symbol, symbol_id) in entries {
if let Some(name) = &symbol.name {
if name == target || name.contains(target) {
matches.push((node_id, symbol, symbol_id));
}
}
}
}
if matches.is_empty() {
return Err(anyhow::anyhow!(
"Target symbol '{}' not found (tried symbol_id, FQN, and name)",
target
));
}
if matches.len() == 1 {
let (_node_id, symbol, symbol_id) = &matches[0];
let fqn = symbol
.canonical_fqn
.as_ref()
.or(symbol.display_fqn.as_ref())
.map(|s| s.as_str())
.unwrap_or_else(|| symbol.name.as_deref().unwrap_or("<unknown>"));
return Ok(ResolvedTarget {
symbol_id: symbol_id.clone().unwrap_or_default(),
fqn: fqn.to_string(),
file_path: symbol.file_path.to_string_lossy().to_string(),
kind: symbol.kind_normalized.clone(),
});
}
eprintln!(
"Ambiguous target '{}': found {} candidates",
target,
matches.len()
);
eprintln!();
eprintln!("Top matches:");
for (i, (_node_id, symbol, symbol_id)) in matches.iter().take(10).enumerate() {
let fqn = symbol
.canonical_fqn
.as_ref()
.or(symbol.display_fqn.as_ref())
.map(|s| s.as_str())
.unwrap_or("<unknown>");
let sid = symbol_id.as_deref().unwrap_or("<none>");
let name = symbol.name.as_deref().unwrap_or("<unknown>");
eprintln!(
" [{}] {} ({}) in {}:{}",
i + 1,
name,
symbol.kind_normalized,
symbol.file_path.display(),
symbol.start_line
);
eprintln!(" Symbol ID: {}", sid);
eprintln!(" FQN: {}", fqn);
}
if matches.len() > 10 {
eprintln!(" ... and {} more", matches.len() - 10);
}
eprintln!();
eprintln!("Use --target <symbol_id> for precise lookup");
Err(anyhow::anyhow!(
"Target '{}' is ambiguous ({} matches)",
target,
matches.len()
))
}
pub fn run_slice(
db_path: PathBuf,
target: String,
direction: CliSliceDirection,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let mut args = vec![
"slice".to_string(),
"--target".to_string(),
target.clone(),
"--direction".to_string(),
direction.as_str().to_string(),
];
if verbose {
args.push("--verbose".to_string());
}
let mut graph = CodeGraph::open(&db_path)?;
let exec_id = magellan::output::generate_execution_id();
let db_path_str = db_path.to_string_lossy().to_string();
graph.execution_log().start_execution(
&exec_id,
env!("CARGO_PKG_VERSION"),
&args,
None,
&db_path_str,
)?;
graph
.telemetry()
.record_phase_start(&exec_id, "resolve_target")?;
let resolved = match resolve_target(&mut graph, &db_path, &target) {
Ok(r) => r,
Err(e) => {
graph
.telemetry()
.record_phase_end(&exec_id, "resolve_target")?;
graph.execution_log().finish_execution(
&exec_id,
"error",
Some(&e.to_string()),
0,
0,
0,
)?;
return Err(e);
}
};
graph
.telemetry()
.record_phase_end(&exec_id, "resolve_target")?;
graph
.telemetry()
.record_phase_start(&exec_id, "compute_slice")?;
let included_symbols = {
let symbols_result = match direction {
CliSliceDirection::Backward => {
graph.reverse_reachable_symbols(&resolved.symbol_id, None)
}
CliSliceDirection::Forward => graph.reachable_symbols(&resolved.symbol_id, None),
};
match symbols_result {
Ok(symbols) => symbols,
Err(e) => {
graph
.telemetry()
.record_phase_end(&exec_id, "compute_slice")?;
graph.execution_log().finish_execution(
&exec_id,
"error",
Some(&e.to_string()),
0,
0,
0,
)?;
return Err(e);
}
}
};
graph
.telemetry()
.record_phase_end(&exec_id, "compute_slice")?;
let target_info = magellan::graph::SymbolInfo {
symbol_id: Some(resolved.symbol_id.clone()),
fqn: Some(resolved.fqn.clone()),
kind: resolved.kind.clone(),
file_path: resolved.file_path.clone(),
};
let slice_result = magellan::graph::SliceResult {
slice: magellan::graph::ProgramSlice {
direction: match direction {
CliSliceDirection::Backward => magellan::graph::SliceDirection::Backward,
CliSliceDirection::Forward => magellan::graph::SliceDirection::Forward,
},
target: target_info.clone(),
included_symbols: included_symbols.clone(),
symbol_count: included_symbols.len(),
},
statistics: magellan::graph::SliceStatistics {
total_symbols: included_symbols.len(),
data_dependencies: 0,
control_dependencies: included_symbols.len().saturating_sub(1),
},
};
if output_format == OutputFormat::Json || output_format == OutputFormat::Pretty {
graph
.execution_log()
.finish_execution(&exec_id, "success", None, 0, 0, 0)?;
return output_json_mode(
&resolved.fqn,
slice_result,
verbose,
&exec_id,
output_format,
);
}
let direction_label = match direction {
CliSliceDirection::Backward => "that affect",
CliSliceDirection::Forward => "affected by",
};
println!(
"Program slice: symbols {} \"{}\"",
direction_label, resolved.fqn
);
println!(" Target: {} ({})", resolved.fqn, resolved.kind);
println!(" File: {}", resolved.file_path);
println!(" Total symbols: {}", slice_result.statistics.total_symbols);
if verbose {
println!(
" Data dependencies: {} (not computed in call-graph fallback)",
slice_result.statistics.data_dependencies
);
println!(
" Control dependencies: {}",
slice_result.statistics.control_dependencies
);
}
if slice_result.slice.included_symbols.is_empty() {
println!("\n No symbols in slice.");
} else {
println!("\n Symbols in slice:");
for symbol in &slice_result.slice.included_symbols {
let fqn_display = symbol.fqn.as_deref().unwrap_or("?");
println!(
" {} ({}) in {}",
fqn_display, symbol.kind, symbol.file_path
);
}
}
if slice_result.statistics.data_dependencies == 0
&& !slice_result.slice.included_symbols.is_empty()
{
println!("\n Note: Current implementation uses call-graph reachability.");
}
graph
.execution_log()
.finish_execution(&exec_id, "success", None, 0, 0, 0)?;
Ok(())
}
fn output_json_mode(
_target: &str,
slice_result: magellan::graph::SliceResult,
_verbose: bool,
exec_id: &str,
output_format: OutputFormat,
) -> Result<()> {
let target_span = Span::new(
slice_result.slice.target.file_path.clone(),
0,
0,
1,
0,
1,
0,
);
let target_symbol_id = slice_result.slice.target.symbol_id.clone();
let target_match = SymbolMatch::new(
slice_result
.slice
.target
.fqn
.unwrap_or_else(|| "?".to_string()),
slice_result.slice.target.kind,
target_span,
None,
target_symbol_id,
);
let included_symbols: Vec<SymbolMatch> = slice_result
.slice
.included_symbols
.into_iter()
.map(|sym| {
let span = Span::new(sym.file_path.clone(), 0, 0, 1, 0, 1, 0);
SymbolMatch::new(
sym.fqn.unwrap_or_else(|| "?".to_string()),
sym.kind,
span,
None,
sym.symbol_id,
)
})
.collect();
let direction = match slice_result.slice.direction {
magellan::graph::SliceDirection::Backward => "backward".to_string(),
magellan::graph::SliceDirection::Forward => "forward".to_string(),
};
let statistics = SliceStats {
total_symbols: slice_result.statistics.total_symbols,
data_dependencies: slice_result.statistics.data_dependencies,
control_dependencies: slice_result.statistics.control_dependencies,
};
let response = SliceResponse {
target: target_match,
direction,
included_symbols,
statistics,
};
let json_response = JsonResponse::new(response, exec_id);
output_json(&json_response, output_format)?;
Ok(())
}