use clap::{Parser, Subcommand, ValueEnum};
pub use crate::analysis::DeadSymbolJson;
#[derive(Parser, Debug, Clone)]
#[command(name = "mirage")]
#[command(author, version, about)]
#[command(
long_about = "Mirage is a path-aware code intelligence engine that operates on graphs, not text.
It materializes behavior explicitly: paths, proofs, counterexamples.
NOT:
- A search tool (llmgrep already does this)
- An embedding tool
- Static analysis / linting
IS:
- Path enumeration and verification
- Graph-based reasoning about code behavior
- Truth engine that materializes facts for LLM consumption
The Golden Rule: An agent may only speak if it can reference a graph artifact."
)]
pub struct Cli {
#[arg(global = true, long, env = "MIRAGE_DB")]
pub db: Option<String>,
#[arg(global = true, long, value_enum, default_value_t = OutputFormat::Human)]
pub output: OutputFormat,
#[arg(long, global = true, default_value = "false")]
pub detect_backend: bool,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
Human,
Json,
Pretty,
}
#[derive(Subcommand, Debug, Clone)]
pub enum Commands {
Status(StatusArgs),
Paths(PathsArgs),
Cfg(CfgArgs),
Dominators(DominatorsArgs),
Loops(LoopsArgs),
Unreachable(UnreachableArgs),
Patterns(PatternsArgs),
Frontiers(FrontiersArgs),
Verify(VerifyArgs),
BlastZone(BlastZoneArgs),
Cycles(CyclesArgs),
Slice(SliceArgs),
Hotspots(HotspotsArgs),
Hotpaths(HotpathsArgs),
Diff(DiffArgs),
Icfg(IcfgArgs),
Coverage(CoverageArgs),
Migrate(MigrateArgs),
}
#[derive(Parser, Debug, Clone, Copy)]
pub struct StatusArgs {}
#[derive(Parser, Debug, Clone)]
pub struct PathsArgs {
#[arg(long)]
pub function: String,
#[arg(long)]
pub file: Option<String>,
#[arg(long)]
pub show_errors: bool,
#[arg(long)]
pub max_length: Option<usize>,
#[arg(long)]
pub with_blocks: bool,
#[arg(long)]
pub incremental: bool,
#[arg(long)]
pub since: Option<String>,
#[arg(long)]
pub by_coverage: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct CfgArgs {
#[arg(long)]
pub function: String,
#[arg(long)]
pub file: Option<String>,
#[arg(long, value_enum)]
pub format: Option<CfgFormat>,
}
#[derive(Parser, Debug, Clone)]
pub struct CoverageArgs {
#[arg(long)]
pub function: String,
#[arg(long)]
pub file: Option<String>,
}
#[derive(Parser, Debug, Clone)]
pub struct DominatorsArgs {
#[arg(long)]
pub function: String,
#[arg(long)]
pub file: Option<String>,
#[arg(long)]
pub must_pass_through: Option<String>,
#[arg(long)]
pub post: bool,
#[arg(long)]
pub inter_procedural: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct LoopsArgs {
#[arg(long)]
pub function: String,
#[arg(long)]
pub file: Option<String>,
#[arg(long)]
pub verbose: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct UnreachableArgs {
#[arg(long)]
pub within_functions: bool,
#[arg(long)]
pub show_branches: bool,
#[arg(long)]
pub include_uncalled: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct PatternsArgs {
#[arg(long)]
pub function: String,
#[arg(long)]
pub file: Option<String>,
#[arg(long)]
pub if_else: bool,
#[arg(long)]
pub r#match: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct FrontiersArgs {
#[arg(long)]
pub function: String,
#[arg(long)]
pub file: Option<String>,
#[arg(long)]
pub iterated: bool,
#[arg(long)]
pub node: Option<usize>,
}
#[derive(Parser, Debug, Clone)]
pub struct VerifyArgs {
#[arg(long)]
pub path_id: String,
}
#[derive(Parser, Debug, Clone)]
pub struct BlastZoneArgs {
#[arg(long)]
pub function: Option<String>,
#[arg(long)]
pub file: Option<String>,
#[arg(long)]
pub block_id: Option<usize>,
#[arg(long)]
pub path_id: Option<String>,
#[arg(long, default_value_t = 100)]
pub max_depth: usize,
#[arg(long)]
pub include_errors: bool,
#[arg(long)]
pub use_call_graph: bool,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum CycleTypeArg {
All,
InterFunction,
SelfLoop,
}
#[derive(Parser, Debug, Clone)]
pub struct CyclesArgs {
#[arg(long)]
pub call_graph: bool,
#[arg(long)]
pub function_loops: bool,
#[arg(long)]
pub both: bool,
#[arg(long, value_enum, default_value = "all")]
pub cycle_type: CycleTypeArg,
#[arg(long)]
pub verbose: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct SliceArgs {
#[arg(long)]
pub symbol: String,
#[arg(long, value_enum)]
pub direction: SliceDirectionArg,
#[arg(long)]
pub verbose: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct HotspotsArgs {
#[arg(long, default_value = "main")]
pub entry: String,
#[arg(long, default_value = "20")]
pub top: usize,
#[arg(long)]
pub min_paths: Option<usize>,
#[arg(long)]
pub verbose: bool,
#[arg(long, default_value = "true")]
pub inter_procedural: bool,
#[arg(long, conflicts_with = "inter_procedural")]
pub intra_procedural: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct HotpathsArgs {
#[arg(long)]
pub function: String,
#[arg(long, default_value = "10")]
pub top: usize,
#[arg(long)]
pub rationale: bool,
#[arg(long)]
pub min_score: Option<f64>,
}
#[derive(Parser, Debug, Clone)]
pub struct MigrateArgs {
#[arg(long, value_enum)]
pub from: BackendFormat,
#[arg(long, value_enum)]
pub to: BackendFormat,
#[arg(short, long)]
pub db: String,
#[arg(long)]
pub backup: bool,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct IcfgArgs {
#[arg(long)]
pub entry: String,
#[arg(long, default_value = "3")]
pub depth: usize,
#[arg(long, default_value = "true")]
pub return_edges: bool,
#[arg(long, value_enum)]
pub format: Option<IcfgFormat>,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum IcfgFormat {
Dot,
Json,
Human,
}
#[derive(Parser, Debug, Clone)]
pub struct DiffArgs {
#[arg(long)]
pub function: String,
#[arg(long)]
pub before: String,
#[arg(long)]
pub after: String,
#[arg(long)]
pub show_edges: bool,
#[arg(long)]
pub verbose: bool,
}
#[derive(clap::ValueEnum, Clone, Debug, Copy, PartialEq, Eq)]
pub enum BackendFormat {
Sqlite,
Geometric,
}
impl std::fmt::Display for BackendFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Sqlite => write!(f, "sqlite"),
Self::Geometric => write!(f, "geometric"),
}
}
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum SliceDirectionArg {
Backward,
Forward,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum CfgFormat {
Human,
Dot,
Json,
}
pub fn resolve_db_path(cli_db: Option<String>) -> anyhow::Result<String> {
if let Some(path) = cli_db {
return Ok(path);
}
if let Ok(path) = std::env::var("MIRAGE_DB") {
return Ok(path);
}
if let Some(path) = auto_discover_db() {
eprintln!("Info: Auto-discovered database at {}", path);
return Ok(path);
}
Err(anyhow::anyhow!(
"No database specified. Use --db, set MIRAGE_DB env var, \
or run from a directory with a .db file"
))
}
fn auto_discover_db() -> Option<String> {
use std::path::Path;
let search_dirs = [".magellan", ".forge", "."];
for dir in &search_dirs {
if let Ok(entries) = std::fs::read_dir(dir) {
let mut db_files: Vec<_> = entries
.filter_map(|e| e.ok())
.filter(|e| {
let path = e.path();
path.extension().map(|ext| ext == "db").unwrap_or(false)
})
.map(|e| e.path())
.collect();
db_files.sort();
if let Some(preferred) = db_files.iter().find(|p| {
let name = p
.file_stem()
.map(|s| s.to_string_lossy())
.unwrap_or_default();
name == "magellan" || name == "mirage"
}) {
return Some(preferred.to_string_lossy().to_string());
}
if let Some(first) = db_files.first() {
return Some(first.to_string_lossy().to_string());
}
}
}
let candidates = [
".magellan/mirage.db",
".magellan/magellan.db",
"mirage.db",
"magellan.db",
"graph.db",
];
for name in &candidates {
if Path::new(name).exists() {
return Some(name.to_string());
}
}
None
}
fn detect_repo_path(db_path: &str) -> std::path::PathBuf {
use std::path::Path;
let db_path = Path::new(db_path);
let mut path = if db_path.is_absolute() {
db_path.to_path_buf()
} else {
std::env::current_dir()
.map(|cwd| cwd.join(db_path))
.unwrap_or_else(|_| db_path.to_path_buf())
};
while path.pop() {
let git_dir = path.join(".git");
if git_dir.exists() {
return path;
}
}
Path::new(".").to_path_buf()
}
#[derive(serde::Serialize)]
struct PathsResponse {
function: String,
total_paths: usize,
error_paths: usize,
paths: Vec<PathSummary>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
struct PathBlock {
block_id: usize,
terminator: String,
}
#[derive(serde::Serialize)]
struct SourceRange {
file_path: String,
start_line: usize,
end_line: usize,
}
#[derive(serde::Serialize)]
struct PathSummary {
path_id: String,
kind: String,
length: usize,
blocks: Vec<PathBlock>,
summary: Option<String>,
source_range: Option<SourceRange>,
}
impl From<crate::cfg::Path> for PathSummary {
fn from(path: crate::cfg::Path) -> Self {
let length = path.len();
let blocks: Vec<PathBlock> = path
.blocks
.into_iter()
.map(|block_id| PathBlock {
block_id,
terminator: "Unknown".to_string(),
})
.collect();
Self {
path_id: path.path_id,
kind: format!("{:?}", path.kind),
length,
blocks,
summary: None, source_range: None, }
}
}
impl PathSummary {
pub fn from_with_cfg(path: crate::cfg::Path, cfg: &crate::cfg::Cfg) -> Self {
use crate::cfg::summarize_path;
let summary = Some(summarize_path(cfg, &path));
let blocks: Vec<PathBlock> = path
.blocks
.iter()
.map(|&block_id| {
let node_idx = cfg.node_indices().find(|&n| cfg[n].id == block_id);
let terminator = match node_idx {
Some(idx) => format!("{:?}", cfg[idx].terminator),
None => "Unknown".to_string(),
};
PathBlock {
block_id,
terminator,
}
})
.collect();
let source_range = Self::calculate_source_range(&path, cfg);
let length = path.len();
Self {
path_id: path.path_id,
kind: format!("{:?}", path.kind),
length,
summary,
source_range,
blocks,
}
}
fn calculate_source_range(
path: &crate::cfg::Path,
cfg: &crate::cfg::Cfg,
) -> Option<SourceRange> {
let first_loc = path
.blocks
.first()
.and_then(|&bid| cfg.node_indices().find(|&n| cfg[n].id == bid))
.and_then(|idx| cfg[idx].source_location.clone());
let last_loc = path
.blocks
.last()
.and_then(|&bid| cfg.node_indices().find(|&n| cfg[n].id == bid))
.and_then(|idx| cfg[idx].source_location.clone());
match (first_loc, last_loc) {
(Some(first), Some(last)) => {
Some(SourceRange {
file_path: first.file_path.to_string_lossy().to_string(),
start_line: first.start_line,
end_line: last.end_line,
})
}
_ => None,
}
}
}
#[derive(serde::Serialize)]
struct DominanceResponse {
function: String,
kind: String, root: Option<usize>,
dominance_tree: Vec<DominatorEntry>,
must_pass_through: Option<MustPassThroughResult>,
}
#[derive(serde::Serialize)]
struct DominatorEntry {
block: usize,
immediate_dominator: Option<usize>,
dominated: Vec<usize>,
}
#[derive(serde::Serialize)]
struct MustPassThroughResult {
block: usize,
must_pass: Vec<usize>,
}
#[derive(serde::Serialize)]
struct InterProceduralDominanceResponse {
function: String,
kind: String,
dominator_count: usize,
dominators: Vec<String>,
}
#[derive(serde::Serialize)]
struct UnreachableResponse {
function: String,
total_functions: usize,
functions_with_unreachable: usize,
unreachable_count: usize,
blocks: Vec<UnreachableBlock>,
#[serde(skip_serializing_if = "Option::is_none")]
uncalled_functions: Option<Vec<DeadSymbolJson>>,
}
#[derive(serde::Serialize, Clone)]
struct IncomingEdge {
from_block: usize,
edge_type: String,
}
#[derive(serde::Serialize, Clone)]
struct UnreachableBlock {
block_id: usize,
kind: String,
statements: Vec<String>,
terminator: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
incoming_edges: Vec<IncomingEdge>,
}
#[derive(serde::Serialize)]
struct VerifyResult {
path_id: String,
valid: bool,
found_in_cache: bool,
function_id: Option<i64>,
reason: String,
current_paths: usize,
}
#[derive(serde::Serialize)]
struct LoopsResponse {
function: String,
loop_count: usize,
loops: Vec<LoopInfo>,
}
#[derive(serde::Serialize)]
struct LoopInfo {
header: usize,
back_edge_from: usize,
body_size: usize,
nesting_level: usize,
body_blocks: Vec<usize>,
}
#[derive(serde::Serialize)]
struct PatternsResponse {
function: String,
if_else_count: usize,
match_count: usize,
if_else_patterns: Vec<IfElseInfo>,
match_patterns: Vec<MatchInfo>,
}
#[derive(serde::Serialize)]
struct IfElseInfo {
condition_block: usize,
true_branch: usize,
false_branch: usize,
merge_point: Option<usize>,
has_else: bool,
}
#[derive(serde::Serialize)]
struct MatchInfo {
switch_block: usize,
branch_count: usize,
targets: Vec<usize>,
otherwise: usize,
}
#[derive(serde::Serialize)]
struct FrontiersResponse {
function: String,
nodes_with_frontiers: usize,
frontiers: Vec<NodeFrontier>,
}
#[derive(serde::Serialize)]
struct NodeFrontier {
node: usize,
frontier_set: Vec<usize>,
}
#[derive(serde::Serialize)]
struct IteratedFrontierResponse {
function: String,
iterated_frontier: Vec<usize>,
}
#[derive(serde::Serialize)]
struct BlockImpactResponse {
function: String,
block_id: usize,
reachable_blocks: Vec<usize>,
reachable_count: usize,
max_depth: usize,
has_cycles: bool,
#[serde(skip_serializing_if = "Option::is_none")]
forward_impact: Option<Vec<CallGraphSymbol>>,
#[serde(skip_serializing_if = "Option::is_none")]
backward_impact: Option<Vec<CallGraphSymbol>>,
}
#[derive(serde::Serialize)]
struct PathImpactResponse {
path_id: String,
path_length: usize,
unique_blocks_affected: Vec<usize>,
impact_count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
forward_impact: Option<Vec<CallGraphSymbol>>,
#[serde(skip_serializing_if = "Option::is_none")]
backward_impact: Option<Vec<CallGraphSymbol>>,
}
#[derive(Clone, serde::Serialize)]
struct CallGraphSymbol {
#[serde(skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
fqn: Option<String>,
file_path: String,
kind: String,
}
#[derive(serde::Serialize)]
struct HotspotsResponse {
entry_point: String,
total_functions: usize,
hotspots: Vec<HotspotEntry>,
mode: String, }
#[derive(serde::Serialize, Clone)]
struct HotspotEntry {
function: String,
risk_score: f64,
path_count: usize,
dominance_factor: f64,
complexity: usize,
file_path: String,
}
pub mod cmds {
use super::*;
use crate::output;
use anyhow::Result;
pub fn status(_args: &StatusArgs, cli: &Cli) -> Result<()> {
use crate::storage::MirageDb;
let db_path = super::resolve_db_path(cli.db.clone())?;
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::error(&format!("Error details: {}", e));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let status = db.status()?;
match cli.output {
OutputFormat::Human => {
println!("Mirage Database Status:");
println!(
" Schema version: {} (Magellan: {})",
status.mirage_schema_version, status.magellan_schema_version
);
println!(" cfg_blocks: {}", status.cfg_blocks);
println!(
" cfg_paths: {} (use 'mirage paths --function <name>' to enumerate)",
status.cfg_paths
);
println!(" cfg_dominators: {}", status.cfg_dominators);
}
OutputFormat::Json => {
let response = output::JsonResponse::new(status);
println!("{}", response.to_json());
}
OutputFormat::Pretty => {
let response = output::JsonResponse::new(status);
println!("{}", response.to_pretty_json());
}
}
Ok(())
}
pub fn paths(args: &PathsArgs, cli: &Cli) -> Result<()> {
use crate::cfg::{
enumerate_paths_incremental, get_or_enumerate_paths, PathKind, PathLimits,
};
use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
use crate::storage::{get_function_hash_db, MirageDb};
let db_path = super::resolve_db_path(cli.db.clone())?;
let repo_path = detect_repo_path(&db_path);
if args.incremental {
let since = args
.since
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--since required with --incremental"))?;
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let result = match enumerate_paths_incremental(
&args.function,
&db,
&repo_path,
since,
args.max_length,
) {
Ok(r) => r,
Err(e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"IncrementalAnalysisError",
&format!("Incremental analysis failed: {}", e),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Incremental analysis failed: {}", e));
std::process::exit(output::EXIT_DATABASE);
}
}
};
match cli.output {
OutputFormat::Human => {
println!("Incremental path enumeration (since {}):", since);
println!(" Analyzed functions: {}", result.analyzed_functions);
println!(" Total paths: {}", result.paths.len());
if args.show_errors {
let error_count = result
.paths
.iter()
.filter(|p| matches!(p.kind, PathKind::Error))
.count();
println!(" Error paths: {}", error_count);
}
if !result.paths.is_empty() {
println!("\nPaths:");
for path in &result.paths {
if args.show_errors || !matches!(path.kind, PathKind::Error) {
println!(" {}", path);
}
}
}
}
OutputFormat::Json => {
let response = serde_json::json!({
"incremental": true,
"since": since,
"analyzed_functions": result.analyzed_functions,
"skipped_functions": result.skipped_functions,
"total_paths": result.paths.len(),
"paths": result.paths,
});
println!("{}", serde_json::to_string(&response)?);
}
OutputFormat::Pretty => {
let response = serde_json::json!({
"incremental": true,
"since": since,
"analyzed_functions": result.analyzed_functions,
"skipped_functions": result.skipped_functions,
"total_paths": result.paths.len(),
"paths": result.paths,
});
println!("{}", serde_json::to_string_pretty(&response)?);
}
}
return Ok(());
}
let mut db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let function_id =
match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
Ok(id) => id,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::function_not_found(&args.function);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Function '{}' not found in database",
args.function
));
output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let cfg = match load_cfg_from_db(&db, function_id) {
Ok(cfg) => cfg,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"CgfLoadError",
&format!("Failed to load CFG for function '{}'", args.function),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Failed to load CFG for function '{}'",
args.function
));
output::info("The function may be corrupted. Try re-running 'magellan watch'");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let mut limits = PathLimits::default();
if let Some(max_length) = args.max_length {
limits = limits.with_max_length(max_length);
}
let mut paths = if db.is_sqlite() {
let function_hash = match get_function_hash_db(&db, function_id) {
Some(hash) => hash,
None => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"HashNotFound",
&format!("Function hash not found for '{}'", args.function),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Function hash not found for '{}'", args.function));
output::info(
"The function data may be incomplete. Try re-running 'magellan watch'",
);
std::process::exit(output::EXIT_DATABASE);
}
}
};
get_or_enumerate_paths(&cfg, function_id, &function_hash, &limits, db.conn_mut()?)
.map_err(|e| anyhow::anyhow!("Path enumeration failed: {}", e))?
} else {
crate::cfg::enumerate_paths(&cfg, &limits)
};
if args.show_errors {
paths.retain(|p| p.kind == PathKind::Error);
}
if args.by_coverage {
let coverage_map: std::collections::HashMap<i64, i64> = db
.conn()
.ok()
.and_then(|conn| {
let sql = "SELECT block_id, hit_count FROM cfg_block_coverage \
WHERE block_id IN (SELECT id FROM cfg_blocks WHERE function_id = ?1)";
let mut stmt = conn.prepare(sql).ok()?;
let rows = stmt.query_map([function_id], |row| {
Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
});
let mut map = std::collections::HashMap::new();
if let Ok(iter) = rows {
for item in iter {
if let Ok((block_id, hit_count)) = item {
map.insert(block_id, hit_count);
}
}
}
if map.is_empty() {
None
} else {
Some(map)
}
})
.unwrap_or_default();
let node_hits: std::collections::HashMap<usize, i64> = cfg
.node_indices()
.filter_map(|idx| {
cfg.node_weight(idx).and_then(|b| {
b.db_id
.and_then(|db_id| coverage_map.get(&db_id).copied())
.map(|hits| (b.id, hits))
})
})
.collect();
paths.sort_by(|a, b| {
let total_a: i64 = a
.blocks
.iter()
.map(|bid| node_hits.get(bid).copied().unwrap_or(0))
.sum();
let total_b: i64 = b
.blocks
.iter()
.map(|bid| node_hits.get(bid).copied().unwrap_or(0))
.sum();
total_b.cmp(&total_a) });
}
let error_count = paths.iter().filter(|p| p.kind == PathKind::Error).count();
match cli.output {
OutputFormat::Human => {
println!("Function: {}", args.function);
println!("Total paths: {}", paths.len());
if args.show_errors {
println!("(Showing error paths only)");
} else {
println!("Error paths: {}", error_count);
}
println!();
if paths.is_empty() {
output::info("No paths found");
return Ok(());
}
for (i, path) in paths.iter().enumerate() {
println!("Path {}: {}", i + 1, path.path_id);
println!(" Kind: {:?}", path.kind);
println!(" Length: {} blocks", path.len());
if args.with_blocks {
println!(
" Blocks: {}",
path.blocks
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(" -> ")
);
}
println!();
}
}
OutputFormat::Json => {
let response = PathsResponse {
function: args.function.clone(),
total_paths: paths.len(),
error_paths: error_count,
paths: paths
.iter()
.map(|p| PathSummary::from_with_cfg(p.clone(), &cfg))
.collect(),
};
let wrapper = output::JsonResponse::new(response);
println!("{}", wrapper.to_json());
}
OutputFormat::Pretty => {
let response = PathsResponse {
function: args.function.clone(),
total_paths: paths.len(),
error_paths: error_count,
paths: paths
.iter()
.map(|p| PathSummary::from_with_cfg(p.clone(), &cfg))
.collect(),
};
let wrapper = output::JsonResponse::new(response);
println!("{}", wrapper.to_pretty_json());
}
}
Ok(())
}
pub fn cfg(args: &CfgArgs, cli: &Cli) -> Result<()> {
use crate::cfg::{export_dot, export_json, CFGExport};
use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
use crate::storage::MirageDb;
let db_path = super::resolve_db_path(cli.db.clone())?;
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let function_id =
match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
Ok(id) => id,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::function_not_found(&args.function);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Function '{}' not found in database",
args.function
));
output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let cfg = match load_cfg_from_db(&db, function_id) {
Ok(cfg) => cfg,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"CgfLoadError",
&format!("Failed to load CFG for function '{}'", args.function),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Failed to load CFG for function '{}'",
args.function
));
output::info("The function may be corrupted. Try re-running 'magellan watch'");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let coverage: Option<std::collections::HashMap<i64, i64>> =
db.conn().ok().and_then(|conn| {
let sql = "SELECT block_id, hit_count FROM cfg_block_coverage \
WHERE block_id IN (SELECT id FROM cfg_blocks WHERE function_id = ?1)";
let mut stmt = conn.prepare(sql).ok()?;
let rows = stmt.query_map([function_id], |row| {
Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
});
let mut map = std::collections::HashMap::new();
if let Ok(iter) = rows {
for item in iter {
if let Ok((block_id, hit_count)) = item {
map.insert(block_id, hit_count);
}
}
}
if map.is_empty() {
None
} else {
Some(map)
}
});
let format = args.format.unwrap_or(match cli.output {
OutputFormat::Human => CfgFormat::Human,
OutputFormat::Json => CfgFormat::Json,
OutputFormat::Pretty => CfgFormat::Json,
});
match format {
CfgFormat::Human | CfgFormat::Dot => {
let dot = export_dot(&cfg);
println!("{}", dot);
}
CfgFormat::Json => {
let export: CFGExport = export_json(&cfg, &args.function, coverage.as_ref());
let response = output::JsonResponse::new(export);
match cli.output {
OutputFormat::Json => println!("{}", response.to_json()),
OutputFormat::Pretty => println!("{}", response.to_pretty_json()),
OutputFormat::Human => println!("{}", response.to_pretty_json()),
}
}
}
Ok(())
}
pub(crate) fn create_test_cfg() -> crate::cfg::Cfg {
use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
use petgraph::graph::DiGraph;
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec!["let x = 1".to_string()],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec!["if x > 0".to_string()],
terminator: Terminator::SwitchInt {
targets: vec![2],
otherwise: 3,
},
source_location: None,
coord_x: 1,
coord_y: 0,
coord_z: 1,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec!["return true".to_string()],
terminator: Terminator::Return,
source_location: None,
coord_x: 2,
coord_y: 0,
coord_z: 2,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec!["return false".to_string()],
terminator: Terminator::Return,
source_location: None,
coord_x: 2,
coord_y: 0,
coord_z: 3,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::TrueBranch);
g.add_edge(b1, b3, EdgeType::FalseBranch);
g
}
pub fn dominators(args: &DominatorsArgs, cli: &Cli) -> Result<()> {
use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
use crate::cfg::{DominatorTree, PostDominatorTree};
use crate::storage::MirageDb;
let db_path = super::resolve_db_path(cli.db.clone())?;
if args.inter_procedural {
return inter_procedural_dominators(args, cli, &db_path);
}
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let function_id =
match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
Ok(id) => id,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::function_not_found(&args.function);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Function '{}' not found in database",
args.function
));
output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let cfg = match load_cfg_from_db(&db, function_id) {
Ok(cfg) => cfg,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"CgfLoadError",
&format!("Failed to load CFG for function '{}'", args.function),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Failed to load CFG for function '{}'",
args.function
));
output::info("The function may be corrupted. Try re-running 'magellan watch'");
std::process::exit(output::EXIT_DATABASE);
}
}
};
if args.post {
let post_dom_tree = match PostDominatorTree::new(&cfg) {
Some(tree) => tree,
None => {
output::error(
"Could not compute post-dominator tree (CFG may have no exit blocks)",
);
std::process::exit(1);
}
};
if let Some(ref block_id_str) = args.must_pass_through {
match block_id_str.parse::<usize>() {
Ok(block_id) => {
let target_node = cfg.node_indices().find(|&n| cfg[n].id == block_id);
let target_node = match target_node {
Some(node) => node,
None => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::block_not_found(block_id);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(1);
} else {
output::error(&format!("Block {} not found in CFG", block_id));
std::process::exit(1);
}
}
};
let must_pass: Vec<usize> = cfg
.node_indices()
.filter(|&n| post_dom_tree.post_dominates(target_node, n))
.map(|n| cfg[n].id)
.collect();
match cli.output {
OutputFormat::Human => {
println!("Function: {}", args.function);
println!(
"Post-Dominator Query: Blocks post-dominated by {}",
block_id
);
println!("Count: {}", must_pass.len());
println!();
if must_pass.is_empty() {
output::info("No blocks are post-dominated by this block");
} else {
println!("Blocks that must pass through {}:", block_id);
for id in &must_pass {
println!(" - Block {}", id);
}
}
}
OutputFormat::Json | OutputFormat::Pretty => {
let response = DominanceResponse {
function: args.function.clone(),
kind: "post-dominators".to_string(),
root: Some(cfg[post_dom_tree.root()].id),
dominance_tree: vec![],
must_pass_through: Some(MustPassThroughResult {
block: block_id,
must_pass,
}),
};
let wrapper = output::JsonResponse::new(response);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => {
println!("{}", wrapper.to_pretty_json())
}
_ => unreachable!(),
}
}
}
return Ok(());
}
Err(_) => {
output::error(&format!("Invalid block ID: {}", block_id_str));
std::process::exit(1);
}
}
}
let dominance_tree: Vec<DominatorEntry> = cfg
.node_indices()
.map(|node| {
let block = cfg[node].id;
let immediate_dominator = post_dom_tree
.immediate_post_dominator(node)
.map(|n| cfg[n].id);
let dominated: Vec<usize> = post_dom_tree
.children(node)
.iter()
.map(|&n| cfg[n].id)
.collect();
DominatorEntry {
block,
immediate_dominator,
dominated,
}
})
.collect();
match cli.output {
OutputFormat::Human => {
println!("Function: {}", args.function);
println!(
"Post-Dominator Tree (root: {})",
cfg[post_dom_tree.root()].id
);
println!();
print_dominator_tree_human(
&cfg,
post_dom_tree.as_dominator_tree(),
post_dom_tree.root(),
0,
true,
);
}
OutputFormat::Json | OutputFormat::Pretty => {
let response = DominanceResponse {
function: args.function.clone(),
kind: "post-dominators".to_string(),
root: Some(cfg[post_dom_tree.root()].id),
dominance_tree,
must_pass_through: None,
};
let wrapper = output::JsonResponse::new(response);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
} else {
let dom_tree = match DominatorTree::new(&cfg) {
Some(tree) => tree,
None => {
output::error("Could not compute dominator tree (CFG may have no entry block)");
std::process::exit(1);
}
};
if let Some(ref block_id_str) = args.must_pass_through {
match block_id_str.parse::<usize>() {
Ok(block_id) => {
let target_node = cfg.node_indices().find(|&n| cfg[n].id == block_id);
let target_node = match target_node {
Some(node) => node,
None => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::block_not_found(block_id);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(1);
} else {
output::error(&format!("Block {} not found in CFG", block_id));
std::process::exit(1);
}
}
};
let must_pass: Vec<usize> = cfg
.node_indices()
.filter(|&n| dom_tree.dominates(target_node, n))
.map(|n| cfg[n].id)
.collect();
match cli.output {
OutputFormat::Human => {
println!("Function: {}", args.function);
println!("Dominator Query: Blocks dominated by {}", block_id);
println!("Count: {}", must_pass.len());
println!();
if must_pass.is_empty() {
output::info("No blocks are dominated by this block");
} else {
println!("Blocks that must pass through {}:", block_id);
for id in &must_pass {
println!(" - Block {}", id);
}
}
}
OutputFormat::Json | OutputFormat::Pretty => {
let response = DominanceResponse {
function: args.function.clone(),
kind: "dominators".to_string(),
root: Some(cfg[dom_tree.root()].id),
dominance_tree: vec![],
must_pass_through: Some(MustPassThroughResult {
block: block_id,
must_pass,
}),
};
let wrapper = output::JsonResponse::new(response);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => {
println!("{}", wrapper.to_pretty_json())
}
_ => unreachable!(),
}
}
}
return Ok(());
}
Err(_) => {
output::error(&format!("Invalid block ID: {}", block_id_str));
std::process::exit(1);
}
}
}
let dominance_tree: Vec<DominatorEntry> = cfg
.node_indices()
.map(|node| {
let block = cfg[node].id;
let immediate_dominator = dom_tree.immediate_dominator(node).map(|n| cfg[n].id);
let dominated: Vec<usize> =
dom_tree.children(node).iter().map(|&n| cfg[n].id).collect();
DominatorEntry {
block,
immediate_dominator,
dominated,
}
})
.collect();
match cli.output {
OutputFormat::Human => {
println!("Function: {}", args.function);
println!("Dominator Tree (root: {})", cfg[dom_tree.root()].id);
println!();
print_dominator_tree_human(&cfg, &dom_tree, dom_tree.root(), 0, false);
}
OutputFormat::Json | OutputFormat::Pretty => {
let response = DominanceResponse {
function: args.function.clone(),
kind: "dominators".to_string(),
root: Some(cfg[dom_tree.root()].id),
dominance_tree,
must_pass_through: None,
};
let wrapper = output::JsonResponse::new(response);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
}
Ok(())
}
fn print_dominator_tree_human(
cfg: &crate::cfg::Cfg,
dom_tree: &crate::cfg::DominatorTree,
node: petgraph::graph::NodeIndex,
depth: usize,
is_post: bool,
) {
let indent = " ".repeat(depth);
let block_id = cfg[node].id;
let kind_label = if is_post {
"post-dominator"
} else {
"dominator"
};
println!("{}Block {} ({})", indent, block_id, kind_label);
for &child in dom_tree.children(node) {
print_dominator_tree_human(cfg, dom_tree, child, depth + 1, is_post);
}
}
fn print_post_dominator_tree_human(
cfg: &crate::cfg::Cfg,
post_dom_tree: &crate::cfg::PostDominatorTree,
node: petgraph::graph::NodeIndex,
depth: usize,
) {
let indent = " ".repeat(depth);
let block_id = cfg[node].id;
println!("{}Block {} (post-dominator)", indent, block_id);
for &child in post_dom_tree.children(node) {
print_post_dominator_tree_human(cfg, post_dom_tree, child, depth + 1);
}
}
fn inter_procedural_dominators(args: &DominatorsArgs, cli: &Cli, db_path: &str) -> Result<()> {
use crate::analysis::MagellanBridge;
use std::collections::{HashMap, HashSet};
let bridge = match MagellanBridge::open(db_path) {
Ok(b) => b,
Err(e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"MagellanUnavailable",
&format!("Magellan database not available: {}", e),
"Run 'magellan watch' to build the call graph",
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Magellan database not available: {}", e));
output::info("Hint: Run 'magellan watch' to build the call graph");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let condensed = match bridge.condense_call_graph() {
Ok(c) => c,
Err(e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"CondensationError",
&format!("Failed to condense call graph: {}", e),
"Ensure the call graph is properly built",
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to condense call graph: {}", e));
output::info("Hint: Ensure the call graph is properly built");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let mut adjacency: HashMap<i64, Vec<i64>> = HashMap::new();
for &(from_id, to_id) in &condensed.graph.edges {
adjacency.entry(from_id).or_default().push(to_id);
}
let mut symbol_to_scc: HashMap<String, i64> = HashMap::new();
let mut scc_members: HashMap<i64, Vec<String>> = HashMap::new();
for supernode in &condensed.graph.supernodes {
let scc_id = supernode.id;
for member in &supernode.members {
if let Some(fqn) = &member.fqn {
symbol_to_scc.insert(fqn.clone(), scc_id);
scc_members.entry(scc_id).or_default().push(fqn.clone());
}
}
}
let mut dominating_functions: Vec<String> = Vec::new();
if let Some(&target_scc_id) = symbol_to_scc.get(&args.function) {
for (&scc_id, _) in &scc_members {
if scc_id != target_scc_id {
let mut visited = HashSet::new();
if can_reach_scc(scc_id, target_scc_id, &adjacency, &mut visited) {
if let Some(members) = scc_members.get(&scc_id) {
dominating_functions.extend(members.clone());
}
}
}
}
}
dominating_functions.sort();
match cli.output {
OutputFormat::Human => {
output::header(&format!("Inter-procedural Dominators: {}", args.function));
output::info("Functions that must execute before this function can be reached");
println!();
if dominating_functions.is_empty() {
println!(
"No dominators found (this may be an entry point or not in call graph)"
);
} else {
println!(
"Found {} dominating function(s):",
dominating_functions.len()
);
println!();
for (i, dominator) in dominating_functions.iter().enumerate() {
println!("{}. {}", i + 1, dominator);
}
println!();
output::info("These functions are on all call paths to the target");
}
}
OutputFormat::Json => {
let response = InterProceduralDominanceResponse {
function: args.function.clone(),
kind: "inter-procedural-dominators".to_string(),
dominator_count: dominating_functions.len(),
dominators: dominating_functions.clone(),
};
let wrapper = output::JsonResponse::new(response);
println!("{}", wrapper.to_json());
}
OutputFormat::Pretty => {
let response = InterProceduralDominanceResponse {
function: args.function.clone(),
kind: "inter-procedural-dominators".to_string(),
dominator_count: dominating_functions.len(),
dominators: dominating_functions.clone(),
};
let wrapper = output::JsonResponse::new(response);
println!("{}", wrapper.to_pretty_json());
}
}
Ok(())
}
fn can_reach_scc(
from: i64,
to: i64,
adjacency: &std::collections::HashMap<i64, Vec<i64>>,
visited: &mut std::collections::HashSet<i64>,
) -> bool {
if from == to {
return true;
}
if visited.contains(&from) {
return false;
}
visited.insert(from);
if let Some(neighbors) = adjacency.get(&from) {
for &neighbor in neighbors {
if can_reach_scc(neighbor, to, adjacency, visited) {
return true;
}
}
}
false
}
pub fn loops(args: &LoopsArgs, cli: &Cli) -> Result<()> {
use crate::cfg::detect_natural_loops;
use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
use crate::storage::MirageDb;
let db_path = super::resolve_db_path(cli.db.clone())?;
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let function_id =
match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
Ok(id) => id,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::function_not_found(&args.function);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Function '{}' not found in database",
args.function
));
output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let cfg = match load_cfg_from_db(&db, function_id) {
Ok(cfg) => cfg,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"CgfLoadError",
&format!("Failed to load CFG for function '{}'", args.function),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Failed to load CFG for function '{}'",
args.function
));
output::info("The function may be corrupted. Try re-running 'magellan watch'");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let natural_loops = detect_natural_loops(&cfg);
let loop_infos: Vec<LoopInfo> = natural_loops
.iter()
.map(|loop_| {
let nesting_level = loop_.nesting_level(&natural_loops);
let body_blocks: Vec<usize> = loop_.body.iter().map(|&node| cfg[node].id).collect();
LoopInfo {
header: cfg[loop_.header].id,
back_edge_from: cfg[loop_.back_edge.0].id,
body_size: loop_.size(),
nesting_level,
body_blocks,
}
})
.collect();
match cli.output {
OutputFormat::Human => {
println!("Function: {}", args.function);
println!("Natural Loops: {}", natural_loops.len());
println!();
if natural_loops.is_empty() {
output::info("No natural loops detected in this function");
} else {
for (i, loop_info) in loop_infos.iter().enumerate() {
println!("Loop {}:", i + 1);
println!(" Header: Block {}", loop_info.header);
println!(" Back edge from: Block {}", loop_info.back_edge_from);
println!(" Body size: {} blocks", loop_info.body_size);
println!(" Nesting level: {}", loop_info.nesting_level);
if args.verbose {
println!(" Body blocks: {:?}", loop_info.body_blocks);
}
println!();
}
}
}
OutputFormat::Json | OutputFormat::Pretty => {
let response = LoopsResponse {
function: args.function.clone(),
loop_count: natural_loops.len(),
loops: loop_infos,
};
let wrapper = output::JsonResponse::new(response);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
Ok(())
}
pub fn unreachable(args: &UnreachableArgs, cli: &Cli) -> Result<()> {
use crate::analysis::DeadSymbolJson;
use crate::analysis::MagellanBridge;
use crate::cfg::load_cfg_from_db;
use crate::cfg::reachability::find_unreachable;
use crate::storage::MirageDb;
use petgraph::visit::EdgeRef;
let db_path = super::resolve_db_path(cli.db.clone())?;
let uncalled_functions: Option<Vec<DeadSymbolJson>> = if args.include_uncalled {
match MagellanBridge::open(&db_path) {
Ok(bridge) => {
match bridge.dead_symbols("main") {
Ok(dead) => {
let json_symbols: Vec<DeadSymbolJson> =
dead.iter().map(|d| d.into()).collect();
Some(json_symbols)
}
Err(e) => {
eprintln!("Warning: Failed to detect uncalled functions: {}", e);
None
}
}
}
Err(e) => {
eprintln!(
"Warning: Could not open Magellan database for --include-uncalled: {}",
e
);
eprintln!("Note: --include-uncalled requires a Magellan code graph database");
None
}
}
} else {
None
};
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
struct FunctionUnreachable {
function_name: String,
function_id: i64,
blocks: Vec<UnreachableBlock>,
}
if !db.is_sqlite() {
output::error("The 'unreachable' command currently requires SQLite backend.");
output::info("Use SQLite backend or run with --help for alternatives.");
std::process::exit(output::EXIT_USAGE);
}
let mut function_rows: Vec<(String, i64)> = Vec::new();
let mut stmt = match db.conn()?.prepare(
"SELECT name, id FROM graph_entities WHERE kind = 'Symbol' AND json_extract(data, '$.kind') = 'Function'") {
Ok(stmt) => stmt,
Err(e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"QueryError",
&format!("Failed to query functions: {}", e),
output::E_DATABASE_NOT_FOUND,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to query functions: {}", e));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let rows_result = stmt.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
});
match rows_result {
Ok(rows) => {
for row in rows {
match row {
Ok((name, id)) => function_rows.push((name, id)),
Err(e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"QueryError",
&format!("Failed to read function row: {}", e),
output::E_DATABASE_NOT_FOUND,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to read function row: {}", e));
std::process::exit(output::EXIT_DATABASE);
}
}
}
}
}
Err(e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"QueryError",
&format!("Failed to execute query: {}", e),
output::E_DATABASE_NOT_FOUND,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to execute query: {}", e));
std::process::exit(output::EXIT_DATABASE);
}
}
}
let mut all_results = Vec::new();
for (function_name, function_id) in function_rows {
match load_cfg_from_db(&db, function_id) {
Ok(cfg) => {
let unreachable_indices = find_unreachable(&cfg);
if !unreachable_indices.is_empty() {
let blocks: Vec<UnreachableBlock> = unreachable_indices
.iter()
.map(|&idx| {
let block = &cfg[idx];
let kind_str = format!("{:?}", block.kind);
let terminator_str = format!("{:?}", block.terminator);
let incoming_edges = if args.show_branches {
cfg.edge_references()
.filter(|edge| edge.target() == idx)
.filter_map(|edge| {
let source_block = &cfg[edge.source()];
cfg.edge_weight(edge.id()).map(|edge_type| {
IncomingEdge {
from_block: source_block.id,
edge_type: format!("{:?}", edge_type),
}
})
})
.collect()
} else {
vec![]
};
UnreachableBlock {
block_id: block.id,
kind: kind_str,
statements: block.statements.clone(),
terminator: terminator_str,
incoming_edges,
}
})
.collect();
all_results.push(FunctionUnreachable {
function_name,
function_id,
blocks,
});
}
}
Err(_) => {
continue;
}
}
}
let total_functions = all_results.len();
let functions_with_unreachable =
all_results.iter().filter(|r| !r.blocks.is_empty()).count();
let total_blocks: usize = all_results.iter().map(|r| r.blocks.len()).sum();
match cli.output {
OutputFormat::Human => {
if let Some(ref uncalled) = uncalled_functions {
println!("Uncalled Functions ({}):", uncalled.len());
for dead in uncalled {
let name = dead.fqn.as_deref().unwrap_or("?");
println!(" - {} ({})", name, dead.kind);
println!(" File: {}", dead.file_path);
println!(" Reason: {}", dead.reason);
}
println!();
}
if total_blocks == 0 {
if uncalled_functions.is_none()
|| uncalled_functions
.as_ref()
.map(|v| v.is_empty())
.unwrap_or(false)
{
output::info("No unreachable code found");
}
return Ok(());
}
println!("Unreachable Code Blocks:");
println!(" Total blocks: {}", total_blocks);
println!(
" Functions with unreachable: {}/{}",
functions_with_unreachable, total_functions
);
println!();
for result in &all_results {
if result.blocks.is_empty() {
continue;
}
println!("Function: {}", result.function_name);
for block in &result.blocks {
println!(" Block {} ({})", block.block_id, block.kind);
if !block.statements.is_empty() {
for stmt in &block.statements {
println!(" - {}", stmt);
}
}
println!(" Terminator: {}", block.terminator);
println!();
}
if args.show_branches {
println!(" Incoming Edges:");
for block in &result.blocks {
if block.incoming_edges.is_empty() {
println!(
" Block {} has no incoming edges (entry or isolated)",
block.block_id
);
} else {
println!(" Block {} incoming edges:", block.block_id);
for edge in &block.incoming_edges {
println!(
" from block {} ({})",
edge.from_block, edge.edge_type
);
}
}
}
println!();
}
}
}
OutputFormat::Json | OutputFormat::Pretty => {
let all_blocks: Vec<UnreachableBlock> =
all_results.iter().flat_map(|r| r.blocks.clone()).collect();
let response = UnreachableResponse {
function: "all".to_string(),
total_functions,
functions_with_unreachable,
unreachable_count: total_blocks,
blocks: all_blocks,
uncalled_functions: uncalled_functions,
};
let wrapper = output::JsonResponse::new(response);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => {}
}
}
}
Ok(())
}
pub fn verify(args: &VerifyArgs, cli: &Cli) -> Result<()> {
use crate::cfg::{enumerate_paths, load_cfg_from_db, PathLimits};
use crate::storage::MirageDb;
use rusqlite::OptionalExtension;
let db_path = super::resolve_db_path(cli.db.clone())?;
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let path_id = &args.path_id;
if !db.is_sqlite() {
let msg = "Path verification requires SQLite backend with path caching.";
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error =
output::JsonError::new("UnsupportedBackend", msg, output::E_INVALID_INPUT);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_USAGE);
} else {
output::error(msg);
output::info("retired binary backend backend does not support path caching.");
std::process::exit(output::EXIT_USAGE);
}
}
let cached_path_info: Option<(String, i64, String)> = db
.conn()?
.query_row(
"SELECT path_id, function_id, path_kind FROM cfg_paths WHERE path_id = ?1",
rusqlite::params![path_id],
|row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, i64>(1)?,
row.get::<_, String>(2)?,
))
},
)
.optional()
.unwrap_or(None);
let (found_in_cache, function_id, _path_kind) = match cached_path_info {
Some((_id, fid, kind)) => (true, fid, kind),
None => {
let result = VerifyResult {
path_id: path_id.clone(),
valid: false,
found_in_cache: false,
function_id: None,
reason: "Path not found in cache".to_string(),
current_paths: 0,
};
match cli.output {
OutputFormat::Human => {
println!("Path ID {}: not found in cache", path_id);
println!(" The path may have been invalidated or never existed.");
}
OutputFormat::Json | OutputFormat::Pretty => {
let wrapper = output::JsonResponse::new(result);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
return Ok(());
}
};
let cfg = match load_cfg_from_db(&db, function_id) {
Ok(cfg) => cfg,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"CgfLoadError",
&format!("Failed to load CFG for function_id {}", function_id),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Failed to load CFG for function_id {}",
function_id
));
output::info(
"The function data may be corrupted. Try re-running 'magellan watch'",
);
std::process::exit(output::EXIT_DATABASE);
}
}
};
let limits = PathLimits::default();
let current_paths = enumerate_paths(&cfg, &limits);
let current_path_count = current_paths.len();
let path_still_valid = current_paths.iter().any(|p| &p.path_id == path_id);
let reason = if path_still_valid {
"Path found in current enumeration".to_string()
} else {
"Path no longer exists in current enumeration (code may have changed)".to_string()
};
let result = VerifyResult {
path_id: path_id.clone(),
valid: path_still_valid,
found_in_cache,
function_id: Some(function_id),
reason,
current_paths: current_path_count,
};
match cli.output {
OutputFormat::Human => {
println!(
"Path ID {}: {}",
path_id,
if result.valid { "valid" } else { "invalid" }
);
println!(
" Found in cache: {}",
if found_in_cache { "yes" } else { "no" }
);
println!(" Status: {}", result.reason);
println!(" Current total paths: {}", current_path_count);
if !path_still_valid {
println!();
output::info("The path may have been invalidated by code changes.");
output::info("Consider re-running path enumeration to update the cache.");
}
}
OutputFormat::Json | OutputFormat::Pretty => {
let wrapper = output::JsonResponse::new(result);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
Ok(())
}
pub fn blast_zone(args: &BlastZoneArgs, cli: &Cli) -> Result<()> {
use crate::cfg::{find_reachable_from_block, load_cfg_from_db, resolve_function_name};
use crate::storage::{compute_path_impact_from_db, get_function_name_db, MirageDb};
use rusqlite::OptionalExtension;
let db_path = super::resolve_db_path(cli.db.clone())?;
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
if let Some(ref path_id) = args.path_id {
if !db.is_sqlite() {
let msg = "Path-based blast-zone requires SQLite backend. Use block-based analysis with --function and --block-id instead.";
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error =
output::JsonError::new("UnsupportedBackend", msg, output::E_INVALID_INPUT);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_USAGE);
} else {
output::error(msg);
output::info("retired binary backend backend does not support path caching. Use: mirage blast-zone --function <name> --block-id <id>");
std::process::exit(output::EXIT_USAGE);
}
}
let path_id_trimmed = path_id.trim();
if path_id_trimmed.len() < 10 {
let msg = format!("Invalid path_id format: '{}'", path_id_trimmed);
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error =
output::JsonError::new("InvalidInput", &msg, output::E_INVALID_INPUT);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_USAGE);
} else {
output::error(&msg);
output::info("Path ID should be a BLAKE3 hash (64 hex characters)");
std::process::exit(output::EXIT_USAGE);
}
}
let (function_id, path_kind): (i64, String) = match db
.conn()?
.query_row(
"SELECT function_id, path_kind FROM cfg_paths WHERE path_id = ?1",
rusqlite::params![path_id_trimmed],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.optional()
{
Ok(Some(data)) => data,
Ok(None) => {
let msg = format!("Path '{}' not found in cache", path_id_trimmed);
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error =
output::JsonError::new("PathNotFound", &msg, output::E_PATH_NOT_FOUND);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_FILE_NOT_FOUND);
} else {
output::error(&msg);
output::info("Hint: Run 'mirage paths' to enumerate paths first");
std::process::exit(output::EXIT_FILE_NOT_FOUND);
}
}
Err(e) => {
let msg = format!("Failed to query path: {}", e);
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"DatabaseError",
&msg,
output::E_DATABASE_NOT_FOUND,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&msg);
std::process::exit(output::EXIT_DATABASE);
}
}
};
if !args.include_errors && path_kind == "error" {
let msg = format!(
"Path '{}' is an error path (use --include-errors to analyze)",
path_id_trimmed
);
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error =
output::JsonError::new("ErrorPathExcluded", &msg, output::E_INVALID_INPUT);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_USAGE);
} else {
output::error(&msg);
output::info("Use --include-errors to include error paths in analysis");
std::process::exit(output::EXIT_USAGE);
}
}
let cfg = match load_cfg_from_db(&db, function_id) {
Ok(cfg) => cfg,
Err(_e) => {
let msg = format!("Failed to load CFG for function_id {}", function_id);
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error =
output::JsonError::new("CgfLoadError", &msg, output::E_CFG_ERROR);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&msg);
output::info(
"The function may be corrupted. Try re-running 'magellan watch'",
);
std::process::exit(output::EXIT_DATABASE);
}
}
};
let function_name = get_function_name_db(&db, function_id)
.unwrap_or_else(|| format!("<function_{}>", function_id));
let max_depth = if args.max_depth == 100 {
None
} else {
Some(args.max_depth)
};
let impact =
match compute_path_impact_from_db(db.conn()?, path_id_trimmed, &cfg, max_depth) {
Ok(impact) => impact,
Err(e) => {
let msg = format!("Failed to compute path impact: {}", e);
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error =
output::JsonError::new("ImpactError", &msg, output::E_CFG_ERROR);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_ERROR);
} else {
output::error(&msg);
std::process::exit(output::EXIT_ERROR);
}
}
};
let (forward_impact, backward_impact): (
Option<Vec<CallGraphSymbol>>,
Option<Vec<CallGraphSymbol>>,
) = if args.use_call_graph {
use crate::analysis::MagellanBridge;
match MagellanBridge::open(&db_path) {
Ok(bridge) => {
let symbol_id = function_name.as_str();
let forward: Option<Vec<CallGraphSymbol>> = bridge
.reachable_symbols(symbol_id)
.map(|symbols| {
symbols
.into_iter()
.map(|s| CallGraphSymbol {
symbol_id: s.symbol_id,
fqn: s.fqn,
file_path: s.file_path,
kind: s.kind,
})
.collect()
})
.ok();
let backward: Option<Vec<CallGraphSymbol>> = bridge
.reverse_reachable_symbols(symbol_id)
.map(|symbols| {
symbols
.into_iter()
.map(|s| CallGraphSymbol {
symbol_id: s.symbol_id,
fqn: s.fqn,
file_path: s.file_path,
kind: s.kind,
})
.collect()
})
.ok();
(forward, backward)
}
Err(e) => {
eprintln!(
"Warning: Could not open Magellan database for call graph analysis: {}",
e
);
eprintln!("Note: --use-call-graph requires a Magellan code graph database");
(None, None)
}
}
} else {
(None, None)
};
match cli.output {
OutputFormat::Human => {
println!("Path Impact Analysis");
println!();
println!("Path ID: {}", impact.path_id);
println!("Function: {}", function_name);
println!("Path kind: {}", path_kind);
println!("Path length: {} blocks", impact.path_length);
println!();
if let Some(ref forward) = forward_impact {
println!("Inter-Procedural Impact (Call Graph):");
println!(" Forward Impact: {} functions reached", forward.len());
for sym in forward {
println!(" - {}", sym.fqn.as_deref().unwrap_or(&sym.file_path));
}
}
if let Some(ref backward) = backward_impact {
if !backward.is_empty() {
println!(
" Backward Impact: {} functions can reach this",
backward.len()
);
for sym in backward {
println!(" - {}", sym.fqn.as_deref().unwrap_or(&sym.file_path));
}
}
}
println!();
println!("Intra-Procedural Impact (CFG):");
println!(" Unique blocks affected: {}", impact.impact_count);
if impact.impact_count > 0 {
println!(" Affected blocks: {:?}", impact.unique_blocks_affected);
} else {
println!(" Affected blocks: (none - path has no downstream impact)");
}
if let Some(depth) = max_depth {
println!(" Max depth: {}", depth);
} else {
println!(" Max depth: unlimited");
}
}
OutputFormat::Json | OutputFormat::Pretty => {
let response = PathImpactResponse {
path_id: impact.path_id.clone(),
path_length: impact.path_length,
unique_blocks_affected: impact.unique_blocks_affected,
impact_count: impact.impact_count,
forward_impact: forward_impact.clone(),
backward_impact: backward_impact.clone(),
};
let wrapper = output::JsonResponse::new(response);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
} else {
let function_ref = args
.function
.as_ref()
.expect("--function is required for block-based analysis");
let function_id = match resolve_function_name(&db, function_ref) {
Ok(id) => id,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::function_not_found(function_ref);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Function '{}' not found in database",
function_ref
));
output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let function_name = get_function_name_db(&db, function_id)
.unwrap_or_else(|| format!("<function_{}>", function_id));
let cfg = match load_cfg_from_db(&db, function_id) {
Ok(cfg) => cfg,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"CgfLoadError",
&format!("Failed to load CFG for function '{}'", function_ref),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Failed to load CFG for function '{}'",
function_ref
));
output::info(
"The function may be corrupted. Try re-running 'magellan watch'",
);
std::process::exit(output::EXIT_DATABASE);
}
}
};
let block_id = args.block_id.unwrap_or(0);
let block_exists = cfg.node_indices().any(|n| cfg[n].id == block_id);
if !block_exists {
let valid_blocks: Vec<usize> = cfg.node_indices().map(|n| cfg[n].id).collect();
let msg = format!(
"Block {} not found in function '{}'. Valid blocks: {:?}",
block_id, function_ref, valid_blocks
);
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error =
output::JsonError::new("BlockNotFound", &msg, output::E_BLOCK_NOT_FOUND);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_VALIDATION);
} else {
output::error(&msg);
std::process::exit(output::EXIT_VALIDATION);
}
}
let max_depth = if args.max_depth == 100 {
None
} else {
Some(args.max_depth)
};
let impact = find_reachable_from_block(&cfg, block_id, max_depth);
let (forward_impact, backward_impact): (
Option<Vec<CallGraphSymbol>>,
Option<Vec<CallGraphSymbol>>,
) = if args.use_call_graph {
use crate::analysis::MagellanBridge;
match MagellanBridge::open(&db_path) {
Ok(bridge) => {
let symbol_id = function_name.as_str();
let forward: Option<Vec<CallGraphSymbol>> = bridge
.reachable_symbols(symbol_id)
.map(|symbols| {
symbols
.into_iter()
.map(|s| CallGraphSymbol {
symbol_id: s.symbol_id,
fqn: s.fqn,
file_path: s.file_path,
kind: s.kind,
})
.collect()
})
.ok();
let backward: Option<Vec<CallGraphSymbol>> = bridge
.reverse_reachable_symbols(symbol_id)
.map(|symbols| {
symbols
.into_iter()
.map(|s| CallGraphSymbol {
symbol_id: s.symbol_id,
fqn: s.fqn,
file_path: s.file_path,
kind: s.kind,
})
.collect()
})
.ok();
(forward, backward)
}
Err(e) => {
eprintln!(
"Warning: Could not open Magellan database for call graph analysis: {}",
e
);
eprintln!("Note: --use-call-graph requires a Magellan code graph database");
(None, None)
}
}
} else {
(None, None)
};
match cli.output {
OutputFormat::Human => {
println!("Block Impact Analysis (Blast Zone)");
println!();
println!("Function: {}", function_name);
println!("Source block: {}", impact.source_block_id);
println!();
if let Some(ref forward) = forward_impact {
println!("Inter-Procedural Impact (Call Graph):");
println!(" Forward Impact: {} functions reached", forward.len());
for sym in forward {
println!(" - {}", sym.fqn.as_deref().unwrap_or(&sym.file_path));
}
}
if let Some(ref backward) = backward_impact {
if !backward.is_empty() {
println!(
" Backward Impact: {} functions can reach this",
backward.len()
);
for sym in backward {
println!(" - {}", sym.fqn.as_deref().unwrap_or(&sym.file_path));
}
}
}
println!();
println!("Intra-Procedural Impact (CFG):");
println!(" Reachable blocks: {}", impact.reachable_count);
if impact.reachable_count > 0 {
println!(" Affected blocks: {:?}", impact.reachable_blocks);
} else {
println!(" Affected blocks: (none - block has no downstream impact)");
}
println!(" Max depth reached: {}", impact.max_depth_reached);
println!(
" Contains cycles: {}",
if impact.has_cycles {
"yes (loop detected)"
} else {
"no"
}
);
if let Some(depth) = max_depth {
println!(" Depth limit: {}", depth);
} else {
println!(" Depth limit: unlimited");
}
}
OutputFormat::Json | OutputFormat::Pretty => {
let response = BlockImpactResponse {
function: function_name,
block_id: impact.source_block_id,
reachable_blocks: impact.reachable_blocks,
reachable_count: impact.reachable_count,
max_depth: impact.max_depth_reached,
has_cycles: impact.has_cycles,
forward_impact: forward_impact.clone(),
backward_impact: backward_impact.clone(),
};
let wrapper = output::JsonResponse::new(response);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
}
Ok(())
}
pub fn cycles(args: &CyclesArgs, cli: &Cli) -> Result<()> {
use crate::analysis::{CycleInfo, EnhancedCycles, LoopInfo, MagellanBridge};
use crate::cfg::detect_natural_loops;
use crate::cfg::load_cfg_from_db;
use crate::storage::MirageDb;
let db_path = super::resolve_db_path(cli.db.clone())?;
let show_call_graph = args.call_graph
|| args.both
|| (!args.call_graph && !args.function_loops && !args.both);
let show_function_loops = args.function_loops
|| args.both
|| (!args.call_graph && !args.function_loops && !args.both);
let mut call_graph_cycles: Vec<CycleInfo> = if show_call_graph {
match MagellanBridge::open(&db_path) {
Ok(bridge) => match bridge.detect_cycles() {
Ok(report) => report.cycles.iter().map(|c| c.into()).collect(),
Err(e) => {
eprintln!("Warning: Failed to detect call graph cycles: {}", e);
vec![]
}
},
Err(e) => {
eprintln!(
"Warning: Could not open Magellan database for call graph cycles: {}",
e
);
eprintln!("Note: Call graph cycles require a Magellan code graph database");
vec![]
}
}
} else {
vec![]
};
call_graph_cycles.retain(|c| match args.cycle_type {
CycleTypeArg::All => true,
CycleTypeArg::InterFunction => c.cycle_type == "MutualRecursion",
CycleTypeArg::SelfLoop => c.cycle_type == "SelfLoop",
});
let mut function_loops_map: std::collections::HashMap<String, Vec<LoopInfo>> =
std::collections::HashMap::new();
if show_function_loops {
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
if !db.is_sqlite() {
output::error(
"The 'cycles' command with --function-loops requires SQLite backend.",
);
output::info(
"retired binary backend backend is not yet supported for this feature.",
);
std::process::exit(output::EXIT_USAGE);
}
let mut stmt = match db.conn()?.prepare(
"SELECT name, id FROM graph_entities WHERE kind = 'Symbol' AND json_extract(data, '$.kind') = 'Function'") {
Ok(stmt) => stmt,
Err(e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"QueryError",
&format!("Failed to query functions: {}", e),
output::E_DATABASE_NOT_FOUND,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to query functions: {}", e));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let rows_result = stmt.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
});
match rows_result {
Ok(rows) => {
for row in rows {
if let Ok((function_name, function_id)) = row {
if let Ok(cfg) = load_cfg_from_db(&db, function_id) {
let natural_loops = detect_natural_loops(&cfg);
if !natural_loops.is_empty() {
let loop_infos: Vec<LoopInfo> = natural_loops
.iter()
.map(|loop_| {
let nesting_level = loop_.nesting_level(&natural_loops);
let body_blocks: Vec<usize> = loop_
.body
.iter()
.map(|&node| cfg[node].id)
.collect();
LoopInfo {
header: cfg[loop_.header].id,
back_edge_from: cfg[loop_.back_edge.0].id,
body_size: loop_.size(),
nesting_level,
body_blocks,
}
})
.collect();
function_loops_map.insert(function_name, loop_infos);
}
}
}
}
}
Err(e) => {
eprintln!("Warning: Failed to execute query: {}", e);
}
}
}
let total_cycles =
call_graph_cycles.len() + function_loops_map.values().map(|v| v.len()).sum::<usize>();
let enhanced_cycles = EnhancedCycles {
call_graph_cycles,
function_loops: function_loops_map.clone(),
total_cycles,
};
match cli.output {
OutputFormat::Human => {
println!("Cycle Detection Report");
println!();
if show_call_graph {
println!(
"Call Graph Cycles (Inter-procedural): {}",
enhanced_cycles.call_graph_cycles.len()
);
if enhanced_cycles.call_graph_cycles.is_empty() {
println!(" No call graph cycles detected");
} else {
for (i, cycle) in enhanced_cycles.call_graph_cycles.iter().enumerate() {
println!(" Cycle {}:", i + 1);
println!(" Type: {}", cycle.cycle_type);
println!(" Size: {} symbols", cycle.size);
if args.verbose {
println!(" Members:");
for member in &cycle.members {
println!(" - {}", member);
}
}
}
}
println!();
}
if show_function_loops {
println!(
"Function Loops (Intra-procedural): {} functions with loops",
enhanced_cycles.function_loops.len()
);
if enhanced_cycles.function_loops.is_empty() {
println!(" No natural loops detected in any function");
} else {
for (function_name, loops) in &enhanced_cycles.function_loops {
println!(" Function: {} ({} loops)", function_name, loops.len());
if args.verbose {
for (i, loop_info) in loops.iter().enumerate() {
println!(" Loop {}:", i + 1);
println!(" Header: Block {}", loop_info.header);
println!(
" Back edge from: Block {}",
loop_info.back_edge_from
);
println!(" Body size: {} blocks", loop_info.body_size);
println!(" Nesting level: {}", loop_info.nesting_level);
println!(" Body blocks: {:?}", loop_info.body_blocks);
}
}
}
}
println!();
}
println!("Total cycles: {}", total_cycles);
}
OutputFormat::Json | OutputFormat::Pretty => {
let wrapper = output::JsonResponse::new(enhanced_cycles);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
Ok(())
}
pub fn slice(args: &SliceArgs, cli: &Cli) -> Result<()> {
use crate::analysis::{MagellanBridge, SliceWrapper};
let db_path = super::resolve_db_path(cli.db.clone())?;
let bridge = match MagellanBridge::open(&db_path) {
Ok(bridge) => bridge,
Err(e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"DatabaseError",
&format!("Failed to open Magellan database: {}", e),
output::E_DATABASE_NOT_FOUND,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open Magellan database: {}", e));
output::info("Note: Program slicing requires a Magellan code graph database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let slice_result: SliceWrapper = match args.direction {
SliceDirectionArg::Backward => bridge.backward_slice(&args.symbol)?,
SliceDirectionArg::Forward => bridge.forward_slice(&args.symbol)?,
};
match cli.output {
OutputFormat::Human => {
println!("Program Slice: {}", slice_result.direction);
println!();
println!("Target:");
println!(
" Symbol: {}",
slice_result.target.fqn.as_deref().unwrap_or(&args.symbol)
);
println!(" Kind: {}", slice_result.target.kind);
println!(" File: {}", slice_result.target.file_path);
println!();
println!("Statistics:");
println!(" Total symbols in slice: {}", slice_result.symbol_count);
println!(
" Data dependencies: {}",
slice_result.statistics.data_dependencies
);
println!(
" Control dependencies: {}",
slice_result.statistics.control_dependencies
);
println!();
if args.verbose {
println!(
"Included symbols ({}):",
slice_result.included_symbols.len()
);
for (i, symbol) in slice_result.included_symbols.iter().enumerate() {
println!(
" {}. {}",
i + 1,
symbol.fqn.as_deref().unwrap_or("<unknown>")
);
println!(" Kind: {}, File: {}", symbol.kind, symbol.file_path);
}
} else {
println!("Use --verbose to see all included symbols");
}
}
OutputFormat::Json | OutputFormat::Pretty => {
let wrapper = output::JsonResponse::new(slice_result);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
Ok(())
}
pub fn hotspots(args: &HotspotsArgs, cli: &Cli) -> Result<()> {
use crate::analysis::MagellanBridge;
#[cfg(feature = "sqlite")]
use crate::cfg::{
enumerate_paths_with_context, load_cfg_from_db_with_conn, EnumerationContext,
PathLimits,
};
#[cfg(feature = "sqlite")]
use crate::storage::MirageDb;
use std::collections::HashMap;
let db_path = super::resolve_db_path(cli.db.clone())?;
#[cfg(feature = "sqlite")]
let mut db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"DatabaseError",
&format!("Failed to open database: {}", e),
output::E_DATABASE_NOT_FOUND,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", e));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let mut hotspots: Vec<HotspotEntry> = Vec::new();
let mut function_count = 0;
if args.inter_procedural {
match MagellanBridge::open(&db_path) {
Ok(bridge) => {
let path_result = bridge.enumerate_paths(&args.entry, None, 50, args.top * 10);
if let Ok(paths) = path_result {
let mut path_counts: HashMap<String, usize> = HashMap::new();
for path in &paths.paths {
for symbol in &path.symbols {
if let Some(fqn) = &symbol.fqn {
*path_counts.entry(fqn.clone()).or_insert(0) += 1;
}
}
}
let condensed = bridge.condense_call_graph();
if let Ok(condensed) = condensed {
let mut scc_sizes: HashMap<String, f64> = HashMap::new();
for supernode in &condensed.graph.supernodes {
let size = supernode.members.len() as f64;
for member in &supernode.members {
if let Some(fqn) = &member.fqn {
scc_sizes.insert(fqn.clone(), size);
}
}
}
for (fqn, path_count) in &path_counts {
if *path_count >= args.min_paths.unwrap_or(1) {
let dominance = scc_sizes.get(fqn).copied().unwrap_or(1.0);
let risk_score = (*path_count as f64) * 1.0 + dominance * 2.0;
hotspots.push(HotspotEntry {
function: fqn.clone(),
risk_score,
path_count: *path_count,
dominance_factor: dominance,
complexity: 0, file_path: "".to_string(),
});
}
}
function_count = path_counts.len();
}
}
}
Err(_) => {
output::warn(
"Magellan database not available, using intra-procedural analysis",
);
}
}
}
#[cfg(feature = "sqlite")]
if hotspots.is_empty() && db.is_sqlite() {
let conn = db.conn_mut()?;
let query = "SELECT DISTINCT cb.function_id, ge.name, ge.file_path
FROM cfg_blocks cb
JOIN graph_entities ge ON cb.function_id = ge.id";
let mut stmt = conn.prepare(query)?;
let function_rows = stmt.query_map([], |row: &rusqlite::Row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
))
})?;
for func_result in function_rows {
if let Ok((func_id, func_name, file_path)) = func_result {
function_count += 1;
if let Ok(cfg) = load_cfg_from_db_with_conn(conn, func_id) {
let ctx = EnumerationContext::new(&cfg);
let limits = PathLimits::quick_analysis();
let paths = enumerate_paths_with_context(&cfg, &limits, &ctx);
let path_count = paths.len();
if path_count < args.min_paths.unwrap_or(1) {
continue;
}
let complexity = cfg.node_count();
let dominance = 1.0; let risk_score = path_count as f64 * 0.5 + complexity as f64 * 0.1;
hotspots.push(HotspotEntry {
function: func_name.clone(),
risk_score,
path_count,
dominance_factor: dominance,
complexity,
file_path,
});
}
}
}
}
hotspots.sort_by(|a, b| b.risk_score.total_cmp(&a.risk_score));
hotspots.truncate(args.top);
let response = HotspotsResponse {
entry_point: args.entry.clone(),
total_functions: function_count,
hotspots: hotspots.clone(),
mode: if args.inter_procedural {
"inter-procedural"
} else {
"intra-procedural"
}
.to_string(),
};
match cli.output {
OutputFormat::Human => {
output::header(&format!(
"Hotspots Analysis (entry: {})",
response.entry_point
));
if response.total_functions == 0 && response.mode == "intra-procedural" {
output::warn("No functions found. This may be because:");
output::info(" 1. The database hasn't been indexed yet");
output::info(" 2. You need to run: magellan watch --db <path>");
output::info(" 3. Try --inter-procedural for call-graph-based analysis");
println!();
}
output::info(&format!(
"Found {} hotspots out of {} functions",
hotspots.len(),
response.total_functions
));
println!();
for (i, hotspot) in hotspots.iter().enumerate() {
println!(
"{}. {} (risk: {:.1})",
i + 1,
hotspot.function,
hotspot.risk_score
);
if args.verbose {
println!(" Paths: {}", hotspot.path_count);
println!(" Dominance: {:.1}", hotspot.dominance_factor);
println!(" Complexity: {}", hotspot.complexity);
}
}
}
OutputFormat::Json => {
let wrapper = output::JsonResponse::new(response);
println!("{}", wrapper.to_json());
}
OutputFormat::Pretty => {
let wrapper = output::JsonResponse::new(response);
println!("{}", wrapper.to_pretty_json());
}
}
Ok(())
}
pub fn hotpaths(args: &HotpathsArgs, cli: &Cli) -> Result<()> {
use crate::cfg::{
detect_natural_loops, enumerate_paths, find_entry,
hotpaths::{compute_hot_paths, HotpathsOptions},
PathLimits,
};
use crate::storage::MirageDb;
let db_path = super::resolve_db_path(cli.db.clone())?;
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let function_id = match db.resolve_function_name(&args.function) {
Ok(id) => id,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::function_not_found(&args.function);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Function '{}' not found in database",
args.function
));
output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let cfg = match db.load_cfg(function_id) {
Ok(cfg) => cfg,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"CfgLoadError",
&format!("Failed to load CFG for function '{}'", args.function),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Failed to load CFG for function '{}'",
args.function
));
output::info("The function may be corrupted. Try re-running 'magellan watch'");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let entry = match find_entry(&cfg) {
Some(entry) => entry,
None => {
output::error(&format!(
"No entry block found for function '{}'",
args.function
));
std::process::exit(output::EXIT_DATABASE);
}
};
let natural_loops = detect_natural_loops(&cfg);
let limits = PathLimits::default();
let paths = enumerate_paths(&cfg, &limits);
if paths.is_empty() {
output::info(&format!("No paths found for function '{}'", args.function));
return Ok(());
}
let options = HotpathsOptions {
top_n: args.top,
include_rationale: args.rationale,
};
let mut hot_paths = match compute_hot_paths(&cfg, &paths, entry, &natural_loops, options) {
Ok(hp) => hp,
Err(e) => {
output::error(&format!("Failed to compute hot paths: {}", e));
std::process::exit(output::EXIT_DATABASE);
}
};
if let Some(min_score) = args.min_score {
hot_paths.retain(|hp| hp.hotness_score >= min_score);
}
match cli.output {
OutputFormat::Human => {
print_hotpaths_human(&hot_paths, args.rationale);
}
OutputFormat::Json => {
println!("{}", serde_json::to_string(&hot_paths)?);
}
OutputFormat::Pretty => {
println!("{}", serde_json::to_string_pretty(&hot_paths)?);
}
}
Ok(())
}
pub fn patterns(args: &PatternsArgs, cli: &Cli) -> Result<()> {
use crate::cfg::{detect_if_else_patterns, detect_match_patterns};
use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
use crate::storage::MirageDb;
let db_path = super::resolve_db_path(cli.db.clone())?;
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let function_id =
match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
Ok(id) => id,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::function_not_found(&args.function);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Function '{}' not found in database",
args.function
));
output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let cfg = match load_cfg_from_db(&db, function_id) {
Ok(cfg) => cfg,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"CgfLoadError",
&format!("Failed to load CFG for function '{}'", args.function),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Failed to load CFG for function '{}'",
args.function
));
output::info("The function may be corrupted. Try re-running 'magellan watch'");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let show_if_else = !args.r#match; let show_match = !args.if_else;
let if_else_patterns = if show_if_else {
detect_if_else_patterns(&cfg)
} else {
vec![]
};
let match_patterns = if show_match {
detect_match_patterns(&cfg)
} else {
vec![]
};
let if_else_infos: Vec<IfElseInfo> = if_else_patterns
.iter()
.map(|p| IfElseInfo {
condition_block: cfg[p.condition].id,
true_branch: cfg[p.true_branch].id,
false_branch: cfg[p.false_branch].id,
merge_point: p.merge_point.map(|n| cfg[n].id),
has_else: p.has_else(),
})
.collect();
let match_infos: Vec<MatchInfo> = match_patterns
.iter()
.map(|p| MatchInfo {
switch_block: cfg[p.switch_node].id,
branch_count: p.branch_count(),
targets: p.targets.iter().map(|n| cfg[*n].id).collect(),
otherwise: cfg[p.otherwise].id,
})
.collect();
match cli.output {
OutputFormat::Human => {
println!("Function: {}", args.function);
println!();
if show_if_else {
println!("If/Else Patterns: {}", if_else_patterns.len());
if if_else_patterns.is_empty() {
output::info("No if/else patterns detected");
} else {
for (i, info) in if_else_infos.iter().enumerate() {
println!(" Pattern {}:", i + 1);
println!(" Condition: Block {}", info.condition_block);
println!(" True branch: Block {}", info.true_branch);
println!(" False branch: Block {}", info.false_branch);
if let Some(merge) = info.merge_point {
println!(" Merge point: Block {}", merge);
println!(" Has else: {}", info.has_else);
} else {
println!(" Merge point: None (no else)");
}
println!();
}
}
println!();
}
if show_match {
println!("Match Patterns: {}", match_patterns.len());
if match_patterns.is_empty() {
output::info("No match patterns detected");
} else {
for (i, info) in match_infos.iter().enumerate() {
println!(" Pattern {}:", i + 1);
println!(" Switch: Block {}", info.switch_block);
println!(" Branch count: {}", info.branch_count);
println!(" Targets: {:?}", info.targets);
println!(" Otherwise: Block {}", info.otherwise);
println!();
}
}
}
}
OutputFormat::Json | OutputFormat::Pretty => {
let response = PatternsResponse {
function: args.function.clone(),
if_else_count: if_else_patterns.len(),
match_count: match_patterns.len(),
if_else_patterns: if_else_infos,
match_patterns: match_infos,
};
let wrapper = output::JsonResponse::new(response);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
Ok(())
}
pub fn frontiers(args: &FrontiersArgs, cli: &Cli) -> Result<()> {
use crate::cfg::{compute_dominance_frontiers, DominatorTree};
use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
use crate::storage::MirageDb;
let db_path = super::resolve_db_path(cli.db.clone())?;
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let function_id =
match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
Ok(id) => id,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::function_not_found(&args.function);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Function '{}' not found in database",
args.function
));
output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let cfg = match load_cfg_from_db(&db, function_id) {
Ok(cfg) => cfg,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"CgfLoadError",
&format!("Failed to load CFG for function '{}'", args.function),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Failed to load CFG for function '{}'",
args.function
));
output::info("The function may be corrupted. Try re-running 'magellan watch'");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let dom_tree = match DominatorTree::new(&cfg) {
Some(tree) => tree,
None => {
output::error("Could not compute dominator tree (CFG may have no entry blocks)");
std::process::exit(1);
}
};
let frontiers = compute_dominance_frontiers(&cfg, dom_tree);
if args.iterated {
let all_nodes: Vec<petgraph::graph::NodeIndex> = cfg.node_indices().collect();
let iterated_frontier = frontiers.iterated_frontier(&all_nodes);
let iterated_blocks: Vec<usize> =
iterated_frontier.iter().map(|&n| cfg[n].id).collect();
match cli.output {
OutputFormat::Human => {
println!("Function: {}", args.function);
println!("Iterated Dominance Frontier:");
println!("Count: {}", iterated_blocks.len());
println!();
if iterated_blocks.is_empty() {
output::info("No iterated dominance frontier (linear CFG)");
} else {
println!("Blocks in iterated frontier:");
for id in &iterated_blocks {
println!(" - Block {}", id);
}
}
}
OutputFormat::Json | OutputFormat::Pretty => {
let response = IteratedFrontierResponse {
function: args.function.clone(),
iterated_frontier: iterated_blocks,
};
let wrapper = output::JsonResponse::new(response);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
} else if let Some(node_id) = args.node {
let target_node = cfg.node_indices().find(|&n| cfg[n].id == node_id);
let target_node = match target_node {
Some(node) => node,
None => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::block_not_found(node_id);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(1);
} else {
output::error(&format!("Block {} not found in CFG", node_id));
std::process::exit(1);
}
}
};
let frontier = frontiers.frontier(target_node);
let frontier_blocks: Vec<usize> = frontier.iter().map(|&n| cfg[n].id).collect();
match cli.output {
OutputFormat::Human => {
println!("Function: {}", args.function);
println!("Dominance Frontier for Block {}:", node_id);
println!("Count: {}", frontier_blocks.len());
println!();
if frontier_blocks.is_empty() {
output::info(&format!("Block {} has empty dominance frontier", node_id));
} else {
println!("Frontier blocks:");
for id in &frontier_blocks {
println!(" - Block {}", id);
}
}
}
OutputFormat::Json | OutputFormat::Pretty => {
let response = FrontiersResponse {
function: args.function.clone(),
nodes_with_frontiers: if frontier_blocks.is_empty() { 0 } else { 1 },
frontiers: vec![NodeFrontier {
node: node_id,
frontier_set: frontier_blocks,
}],
};
let wrapper = output::JsonResponse::new(response);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
} else {
let nodes_with_frontiers: Vec<NodeFrontier> = frontiers
.nodes_with_frontiers()
.map(|n| {
let frontier = frontiers.frontier(n);
NodeFrontier {
node: cfg[n].id,
frontier_set: frontier.iter().map(|&f| cfg[f].id).collect(),
}
})
.collect();
match cli.output {
OutputFormat::Human => {
println!("Function: {}", args.function);
println!(
"Nodes with non-empty dominance frontiers: {}",
nodes_with_frontiers.len()
);
println!();
if nodes_with_frontiers.is_empty() {
output::info("No dominance frontiers (linear CFG)");
} else {
for node_info in &nodes_with_frontiers {
println!("Block {}:", node_info.node);
println!(" Frontier: {:?}", node_info.frontier_set);
println!();
}
}
}
OutputFormat::Json | OutputFormat::Pretty => {
let response = FrontiersResponse {
function: args.function.clone(),
nodes_with_frontiers: nodes_with_frontiers.len(),
frontiers: nodes_with_frontiers,
};
let wrapper = output::JsonResponse::new(response);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
}
Ok(())
}
pub fn diff(args: &DiffArgs, cli: &Cli) -> Result<()> {
use crate::cfg::diff::compute_cfg_diff;
use crate::storage::MirageDb;
let db_path = super::resolve_db_path(cli.db.clone())?;
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let function_id = match db.resolve_function_name(&args.function) {
Ok(id) => id,
Err(e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new("Database", &e.to_string(), "E001");
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to resolve function: {}", e));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let diff = match compute_cfg_diff(db.storage(), function_id, &args.before, &args.after) {
Ok(diff) => diff,
Err(e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new("Database", &e.to_string(), "E001");
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
return Err(e);
}
}
};
match cli.output {
OutputFormat::Human => print_diff_human(&diff, args.show_edges, args.verbose),
OutputFormat::Json => {
let wrapper = output::JsonResponse::new(diff);
println!("{}", wrapper.to_json());
}
OutputFormat::Pretty => {
let wrapper = output::JsonResponse::new(diff);
println!("{}", wrapper.to_pretty_json());
}
}
Ok(())
}
fn print_diff_human(diff: &crate::cfg::diff::CfgDiff, show_edges: bool, verbose: bool) {
use crate::output::{info, success, warn};
info(&format!("CFG Diff: {}", diff.function_name));
println!(" Before: {}", diff.before_snapshot);
println!(" After: {}", diff.after_snapshot);
let similarity_pct = diff.structural_similarity * 100.0;
if similarity_pct >= 90.0 {
success(&format!(" Similarity: {:.1}%", similarity_pct));
} else if similarity_pct >= 70.0 {
println!(" Similarity: {:.1}%", similarity_pct);
} else {
warn(&format!(" Similarity: {:.1}%", similarity_pct));
}
if !diff.added_blocks.is_empty() {
println!();
info(&format!("Added blocks ({}):", diff.added_blocks.len()));
for block in &diff.added_blocks {
println!(
" + Block {}: {} @ {}",
block.block_id, block.kind, block.source_location
);
}
}
if !diff.deleted_blocks.is_empty() {
println!();
info(&format!("Deleted blocks ({}):", diff.deleted_blocks.len()));
for block in &diff.deleted_blocks {
println!(
" - Block {}: {} @ {}",
block.block_id, block.kind, block.source_location
);
}
}
if !diff.modified_blocks.is_empty() && verbose {
println!();
info(&format!(
"Modified blocks ({}):",
diff.modified_blocks.len()
));
for change in &diff.modified_blocks {
match &change.change_type {
crate::cfg::diff::ChangeType::TerminatorChanged { before, after } => {
println!(" ~ Block {}: {} -> {}", change.block_id, before, after);
}
crate::cfg::diff::ChangeType::SourceLocationChanged => {
println!(" ~ Block {}: location changed", change.block_id);
}
crate::cfg::diff::ChangeType::BothChanged => {
println!(
" ~ Block {}: terminator and location changed",
change.block_id
);
}
crate::cfg::diff::ChangeType::EdgesChanged => {
println!(" ~ Block {}: edges changed", change.block_id);
}
}
}
}
if show_edges {
if !diff.added_edges.is_empty() {
println!();
info(&format!("Added edges ({}):", diff.added_edges.len()));
for edge in &diff.added_edges {
println!(
" + {} -> {} ({})",
edge.from_block, edge.to_block, edge.edge_type
);
}
}
if !diff.deleted_edges.is_empty() {
println!();
info(&format!("Deleted edges ({}):", diff.deleted_edges.len()));
for edge in &diff.deleted_edges {
println!(
" - {} -> {} ({})",
edge.from_block, edge.to_block, edge.edge_type
);
}
}
}
if diff.added_blocks.is_empty()
&& diff.deleted_blocks.is_empty()
&& diff.modified_blocks.is_empty()
&& diff.added_edges.is_empty()
&& diff.deleted_edges.is_empty()
{
println!();
success("No changes detected");
}
}
pub fn icfg(args: &IcfgArgs, cli: &Cli) -> Result<()> {
use crate::cfg::icfg::{build_icfg, to_dot, IcfgJson, IcfgOptions};
use crate::output::error;
use crate::output::{EXIT_DATABASE, EXIT_NOT_FOUND};
use crate::storage::MirageDb;
let db_path = super::resolve_db_path(cli.db.clone())?;
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(e) => {
error(&format!("Failed to open database: {}", e));
std::process::exit(EXIT_DATABASE);
}
};
let function_id = match db.resolve_function_name(&args.entry) {
Ok(id) => id,
Err(_) => {
error(&format!("Function not found: {}", args.entry));
std::process::exit(EXIT_NOT_FOUND);
}
};
let options = IcfgOptions {
max_depth: args.depth,
include_return_edges: args.return_edges,
};
let icfg = match build_icfg(db.storage(), db.backend(), function_id, options) {
Ok(icfg) => icfg,
Err(e) => {
error(&format!("Failed to build ICFG: {}", e));
std::process::exit(EXIT_DATABASE);
}
};
let format = args.format.unwrap_or(match cli.output {
OutputFormat::Human => IcfgFormat::Human,
_ => IcfgFormat::Dot,
});
match format {
IcfgFormat::Dot => {
println!("{}", to_dot(&icfg));
}
IcfgFormat::Json => {
let json_repr = IcfgJson::from_icfg(&icfg);
println!("{}", serde_json::to_string_pretty(&json_repr)?);
}
IcfgFormat::Human => {
print_icfg_human(&icfg);
}
}
Ok(())
}
fn print_icfg_human(icfg: &crate::cfg::icfg::Icfg) {
use std::collections::HashSet;
println!("Inter-Procedural CFG");
println!(" Entry function: {}", icfg.entry_function);
let mut functions = HashSet::new();
for node in icfg.graph.node_indices() {
functions.insert(icfg.graph[node].function_id);
}
println!(" Functions: {}", functions.len());
println!(" Nodes: {}", icfg.graph.node_count());
println!(" Edges: {}", icfg.graph.edge_count());
let mut call_count = 0;
let mut return_count = 0;
let mut intra_count = 0;
for edge in icfg.graph.edge_indices() {
match &icfg.graph[edge] {
crate::cfg::icfg::IcfgEdge::Call { .. } => call_count += 1,
crate::cfg::icfg::IcfgEdge::Return { .. } => return_count += 1,
crate::cfg::icfg::IcfgEdge::IntraProcedural { .. } => intra_count += 1,
}
}
println!(" Edges by type:");
println!(" Call: {}", call_count);
println!(" Return: {}", return_count);
println!(" Intra-procedural: {}", intra_count);
}
pub fn migrate(args: &MigrateArgs, cli: &Cli) -> Result<()> {
use crate::storage::BackendFormat as StorageBackendFormat;
let db_path = std::path::Path::new(&args.db);
if !db_path.exists() {
return Err(anyhow::anyhow!("Database not found: {}", args.db));
}
let actual_format = StorageBackendFormat::detect(db_path)
.map_err(|e| anyhow::anyhow!("Backend detection failed: {}", e))?;
let actual_format_cli = match actual_format {
StorageBackendFormat::SQLite => BackendFormat::Sqlite,
StorageBackendFormat::Geometric => BackendFormat::Geometric,
StorageBackendFormat::Unknown => {
return Err(anyhow::anyhow!(
"Cannot detect backend format: unknown format"
));
}
};
if args.from != actual_format_cli {
return Err(anyhow::anyhow!(
"Source backend mismatch: expected {}, found {:?}",
args.from,
actual_format
));
}
if args.from == args.to {
return Err(anyhow::anyhow!(
"Source and target backends must be different"
));
}
if args.dry_run {
match cli.output {
OutputFormat::Human => {
println!("Dry run: would migrate {} -> {}", args.from, args.to);
println!("Database: {}", args.db);
}
OutputFormat::Json | OutputFormat::Pretty => {
let output = serde_json::json!({
"dry_run": true,
"from": args.from.to_string(),
"to": args.to.to_string(),
"database": args.db,
});
match cli.output {
OutputFormat::Json => println!("{}", serde_json::to_string(&output)?),
OutputFormat::Pretty => {
println!("{}", serde_json::to_string_pretty(&output)?)
}
_ => unreachable!(),
}
}
}
return Ok(());
}
if args.backup {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or_else(|_| {
std::time::SystemTime::now()
.elapsed()
.map(|d| d.as_secs())
.unwrap_or(0)
});
let backup_path = format!("{}.backup.{}", args.db, timestamp);
std::fs::copy(&args.db, &backup_path)
.map_err(|e| anyhow::anyhow!("Failed to create backup: {}", e))?;
eprintln!("Backup created: {}", backup_path);
}
Err(anyhow::anyhow!(
"Backend migration is not supported by this Mirage build; use SQLite .magellan/<project>.db databases"
))
}
pub fn coverage(args: &CoverageArgs, cli: &Cli) -> Result<()> {
use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
use crate::storage::MirageDb;
let db_path = super::resolve_db_path(cli.db.clone())?;
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let function_id =
match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
Ok(id) => id,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::function_not_found(&args.function);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Function '{}' not found in database",
args.function
));
output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let cfg = match load_cfg_from_db(&db, function_id) {
Ok(cfg) => cfg,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"CgfLoadError",
&format!("Failed to load CFG for function '{}'", args.function),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Failed to load CFG for function '{}'",
args.function
));
output::info("The function may be corrupted. Try re-running 'magellan watch'");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let coverage_rows: Vec<(usize, String, i64)> =
db.conn().ok().map_or_else(Vec::new, |conn| {
let sql = "SELECT bb.id, bb.kind, COALESCE(bc.hit_count, 0) \
FROM cfg_blocks bb \
LEFT JOIN cfg_block_coverage bc ON bb.id = bc.block_id \
WHERE bb.function_id = ?1 \
ORDER BY bb.byte_start";
let mut stmt = match conn.prepare(sql) {
Ok(s) => s,
Err(_) => return Vec::new(),
};
let rows = stmt.query_map([function_id], |row| {
Ok((
row.get::<_, i64>(0)? as usize,
row.get::<_, String>(1)?,
row.get::<_, i64>(2)?,
))
});
match rows {
Ok(iter) => iter.filter_map(|r| r.ok()).collect(),
Err(_) => Vec::new(),
}
});
let db_id_to_graph_id: std::collections::HashMap<i64, usize> = cfg
.node_indices()
.filter_map(|idx| {
cfg.node_weight(idx)
.and_then(|b| b.db_id.map(|db_id| (db_id, b.id)))
})
.collect();
match cli.output {
OutputFormat::Human => {
println!("Coverage for function '{}'", args.function);
println!("{}", "=".repeat(60));
if coverage_rows.is_empty() {
println!("No coverage data available.");
println!("Hint: Run tests with 'cargo test' to generate coverage.");
} else {
for (db_id, kind, hits) in &coverage_rows {
let graph_id = db_id_to_graph_id
.get(&(*db_id as i64))
.map(|id| id.to_string())
.unwrap_or_else(|| "?".to_string());
println!(
" Block {:>3} (graph #{}, kind={:>8}): {:>6} hits",
db_id, graph_id, kind, hits
);
}
}
}
OutputFormat::Json | OutputFormat::Pretty => {
#[derive(serde::Serialize)]
struct CoverageEntry {
block_id: usize,
graph_id: Option<usize>,
kind: String,
hit_count: i64,
}
let entries: Vec<CoverageEntry> = coverage_rows
.iter()
.map(|(db_id, kind, hits)| CoverageEntry {
block_id: *db_id,
graph_id: db_id_to_graph_id.get(&(*db_id as i64)).copied(),
kind: kind.to_string(),
hit_count: *hits,
})
.collect();
let response = output::JsonResponse::new(serde_json::json!({
"function": args.function,
"coverage": entries,
}));
match cli.output {
OutputFormat::Json => println!("{}", response.to_json()),
OutputFormat::Pretty => println!("{}", response.to_pretty_json()),
_ => unreachable!(),
}
}
}
Ok(())
}
}
fn print_hotpaths_human(hot_paths: &[crate::cfg::hotpaths::HotPath], show_rationale: bool) {
use crate::output;
output::header(&format!("Hot Paths (top {})", hot_paths.len()));
if hot_paths.is_empty() {
output::info("No hot paths found");
return;
}
for (i, hp) in hot_paths.iter().enumerate() {
println!(
"\n{}. Path {} - Score: {:.2}",
i + 1,
hp.path_id,
hp.hotness_score
);
if show_rationale && !hp.rationale.is_empty() {
println!(" Rationale:");
for r in &hp.rationale {
println!(" - {}", r);
}
}
println!(" Blocks: {} blocks", hp.blocks.len());
for (j, block) in hp.blocks.iter().enumerate() {
if j < 5 || j == hp.blocks.len() - 1 {
print!(" {}", block);
if j == 4 && hp.blocks.len() > 6 {
println!(" ... (+{} more)", hp.blocks.len() - 6);
break;
} else {
println!();
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn clear_env() {
std::env::remove_var("MIRAGE_DB");
}
#[test]
fn test_resolve_db_path_no_source() {
clear_env();
let result = resolve_db_path(None);
match result {
Ok(path) => {
assert!(
path.ends_with(".db") || path.ends_with(".db"),
"Auto-discovered DB should have .db or .db extension"
);
}
Err(_) => {
}
}
}
#[test]
fn test_resolve_db_path_with_cli_arg() {
clear_env();
let result = resolve_db_path(Some("/custom/path.db".to_string())).unwrap();
assert_eq!(result, "/custom/path.db");
}
#[test]
fn test_resolve_db_path_with_env_var() {
clear_env();
std::env::set_var("MIRAGE_DB", "/env/path.db");
let result = resolve_db_path(None).unwrap();
assert_eq!(result, "/env/path.db");
std::env::remove_var("MIRAGE_DB");
}
#[test]
fn test_resolve_db_path_cli_overrides_env() {
clear_env();
std::env::set_var("MIRAGE_DB", "/env/path.db");
let result = resolve_db_path(Some("/cli/path.db".to_string())).unwrap();
assert_eq!(result, "/cli/path.db");
std::env::remove_var("MIRAGE_DB");
}
}
#[cfg(test)]
mod cfg_tests {
use super::*;
use crate::cfg::{export_dot, export_json};
#[test]
fn test_cfg_dot_format() {
let cfg = cmds::create_test_cfg();
let dot = export_dot(&cfg);
assert!(
dot.contains("digraph CFG"),
"DOT output should contain 'digraph CFG'"
);
assert!(
dot.contains("rankdir=TB"),
"DOT output should contain rankdir attribute"
);
assert!(
dot.contains("node [shape=box"),
"DOT output should contain node shape attribute"
);
assert!(
dot.contains("}"),
"DOT output should end with closing brace"
);
assert!(dot.contains("->"), "DOT output should contain edge arrows");
}
#[test]
fn test_cfg_json_format() {
let cfg = cmds::create_test_cfg();
let function_name = "test_function";
let export = export_json(&cfg, function_name, None);
assert_eq!(
export.function_name, function_name,
"JSON export should include function name"
);
assert!(
export.entry.is_some(),
"JSON export should have an entry block"
);
assert!(
!export.exits.is_empty(),
"JSON export should have exit blocks"
);
assert!(!export.blocks.is_empty(), "JSON export should have blocks");
assert!(!export.edges.is_empty(), "JSON export should have edges");
let json_str = serde_json::to_string(&export);
assert!(
json_str.is_ok(),
"JSON export should be serializable to JSON"
);
let json = json_str.unwrap();
assert!(
json.contains(function_name),
"JSON output should contain function name"
);
assert!(
json.contains("\"entry\""),
"JSON output should contain entry field"
);
assert!(
json.contains("\"exits\""),
"JSON output should contain exits field"
);
assert!(
json.contains("\"blocks\""),
"JSON output should contain blocks field"
);
assert!(
json.contains("\"edges\""),
"JSON output should contain edges field"
);
}
#[test]
fn test_cfg_function_name_in_export() {
let cfg = cmds::create_test_cfg();
let test_names = vec!["my_function", "TestFunc", "module::submodule::function"];
for name in test_names {
let export = export_json(&cfg, name, None);
assert_eq!(
export.function_name, name,
"Function name should be preserved in export"
);
}
}
#[test]
fn test_cfg_format_fallback() {
let cli_human = Cli {
db: None,
output: OutputFormat::Human,
command: Some(Commands::Cfg(CfgArgs {
function: "test".to_string(),
file: None,
format: None,
})),
detect_backend: false,
};
let cfg_args = match &cli_human.command {
Some(Commands::Cfg(args)) => args,
_ => panic!("Expected Cfg command"),
};
let resolved_format = cfg_args.format.unwrap_or(match cli_human.output {
OutputFormat::Human => CfgFormat::Human,
OutputFormat::Json => CfgFormat::Json,
OutputFormat::Pretty => CfgFormat::Json,
});
assert_eq!(
resolved_format,
CfgFormat::Human,
"Should fall back to Human format"
);
let cli_json = Cli {
db: None,
output: OutputFormat::Json,
command: Some(Commands::Cfg(CfgArgs {
function: "test".to_string(),
file: None,
format: None,
})),
detect_backend: false,
};
let cfg_args_json = match &cli_json.command {
Some(Commands::Cfg(args)) => args,
_ => panic!("Expected Cfg command"),
};
let resolved_format_json = cfg_args_json.format.unwrap_or(match cli_json.output {
OutputFormat::Human => CfgFormat::Human,
OutputFormat::Json => CfgFormat::Json,
OutputFormat::Pretty => CfgFormat::Json,
});
assert_eq!(
resolved_format_json,
CfgFormat::Json,
"Should fall back to Json format"
);
}
#[test]
fn test_cfg_json_response_wrapper() {
use crate::output::JsonResponse;
let cfg = cmds::create_test_cfg();
let export = export_json(&cfg, "wrapped_function", None);
let response = JsonResponse::new(export);
assert_eq!(response.schema_version, "1.0.1");
assert_eq!(response.tool, "mirage");
assert!(!response.execution_id.is_empty());
assert!(!response.timestamp.is_empty());
let json = response.to_json();
assert!(json.contains("\"schema_version\""));
assert!(json.contains("\"execution_id\""));
assert!(json.contains("\"tool\":\"mirage\""));
assert!(json.contains("\"data\""));
assert!(json.contains("wrapped_function"));
}
#[test]
fn test_cfg_dot_block_info() {
let cfg = cmds::create_test_cfg();
let dot = export_dot(&cfg);
assert!(
dot.contains("lightgreen"),
"DOT should mark entry block with green"
);
assert!(
dot.contains("lightcoral"),
"DOT should mark exit blocks with coral"
);
assert!(dot.contains("Block"), "DOT should contain block labels");
}
#[test]
fn test_cfg_dot_edge_info() {
let cfg = cmds::create_test_cfg();
let dot = export_dot(&cfg);
assert!(
dot.contains("color=green"),
"DOT should show true branch edges in green"
);
assert!(
dot.contains("color=red"),
"DOT should show false branch edges in red"
);
}
}
#[cfg(test)]
mod status_tests {
use crate::storage::{create_schema, MirageDb};
use rusqlite::{params, Connection};
fn create_test_db() -> anyhow::Result<(tempfile::NamedTempFile, MirageDb)> {
use crate::storage::{
REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
};
let file = tempfile::NamedTempFile::new()?;
let mut conn = Connection::open(file.path())?;
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)?;
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)?;
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, ?, ?, ?)",
params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
)?;
create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION)?;
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
params!("function", "test_func", "test.rs", "{}"),
)?;
let function_id: i64 = conn.last_insert_rowid();
conn.execute(
"INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end, start_line, start_col, end_line, end_col)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
params!(function_id, "entry", "goto", 0, 10, 1, 0, 1, 10),
)?;
conn.execute(
"INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end, start_line, start_col, end_line, end_col)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
params!(function_id, "return", "return", 10, 20, 2, 0, 2, 10),
)?;
conn.execute(
"INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)",
params!("test_path", function_id, "normal", 1, 2, 2, 0),
)?;
conn.execute(
"INSERT INTO cfg_dominators (block_id, dominator_id, is_strict) VALUES (?, ?, ?)",
params!(1, 1, false),
)?;
let db = MirageDb::open(file.path())?;
Ok((file, db))
}
#[test]
#[cfg(feature = "backend-sqlite")]
#[allow(deprecated)]
fn test_status_returns_correct_statistics() {
let (_file, db) = create_test_db().unwrap();
let status = db.status().unwrap();
assert_eq!(status.cfg_blocks, 2, "Should have 2 cfg_blocks");
assert_eq!(
status.cfg_edges, 0,
"cfg_edges count should be 0 (managed by Magellan)"
);
assert_eq!(status.cfg_paths, 1, "Should have 1 cfg_path");
assert_eq!(status.cfg_dominators, 1, "Should have 1 cfg_dominator");
assert_eq!(
status.mirage_schema_version, 1,
"Schema version should be 1"
);
assert_eq!(
status.magellan_schema_version, 7,
"Magellan version should be 7"
);
}
#[test]
#[cfg(feature = "backend-sqlite")]
#[allow(deprecated)]
fn test_status_human_output_format() {
let (_file, db) = create_test_db().unwrap();
let status = db.status().unwrap();
assert!(status.cfg_blocks >= 0, "cfg_blocks should be non-negative");
assert!(status.cfg_edges >= 0, "cfg_edges should be non-negative");
assert!(status.cfg_paths >= 0, "cfg_paths should be non-negative");
assert!(
status.cfg_dominators >= 0,
"cfg_dominators should be non-negative"
);
assert!(
status.mirage_schema_version > 0,
"mirage_schema_version should be positive"
);
assert!(
status.magellan_schema_version > 0,
"magellan_schema_version should be positive"
);
}
#[test]
#[cfg(feature = "backend-sqlite")]
fn test_status_json_output_format() {
use crate::output::JsonResponse;
let (_file, db) = create_test_db().unwrap();
let status = db.status().unwrap();
let response = JsonResponse::new(status);
assert_eq!(response.schema_version, "1.0.1");
assert_eq!(response.tool, "mirage");
assert!(!response.execution_id.is_empty());
assert!(!response.timestamp.is_empty());
let json = response.to_json();
assert!(json.contains("\"schema_version\":\"1.0.1\""));
assert!(json.contains("\"tool\":\"mirage\""));
assert!(json.contains("\"execution_id\""));
assert!(json.contains("\"timestamp\""));
assert!(json.contains("\"data\""));
assert!(json.contains("\"cfg_blocks\""));
assert!(json.contains("\"cfg_edges\""));
assert!(json.contains("\"cfg_paths\""));
assert!(json.contains("\"cfg_dominators\""));
assert!(json.contains("\"mirage_schema_version\""));
assert!(json.contains("\"magellan_schema_version\""));
}
#[test]
#[cfg(feature = "backend-sqlite")]
fn test_status_pretty_json_output_format() {
use crate::output::JsonResponse;
let (_file, db) = create_test_db().unwrap();
let status = db.status().unwrap();
let response = JsonResponse::new(status);
let pretty_json = response.to_pretty_json();
assert!(
pretty_json.contains("\n"),
"Pretty JSON should contain newlines"
);
assert!(
pretty_json.contains(" "),
"Pretty JSON should contain indentation"
);
let parsed: serde_json::Value =
serde_json::from_str(&pretty_json).expect("Pretty JSON should be valid");
assert!(parsed.is_object(), "Parsed JSON should be an object");
assert_eq!(parsed["schema_version"], "1.0.1");
assert_eq!(parsed["tool"], "mirage");
assert!(parsed["data"].is_object(), "data field should be an object");
}
#[test]
fn test_status_database_open_error() {
use crate::storage::MirageDb;
let result = MirageDb::open("/nonexistent/path/to/database.db");
match result {
Ok(_) => panic!("Should fail to open non-existent database"),
Err(e) => {
let err_msg = e.to_string();
assert!(
err_msg.contains("Database not found") || err_msg.contains("not found"),
"Error message should mention database not found: {}",
err_msg
);
}
}
}
#[test]
#[cfg(feature = "backend-sqlite")]
#[allow(deprecated)]
fn test_status_empty_database_returns_zeros() {
use crate::storage::{
REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
};
let file = tempfile::NamedTempFile::new().unwrap();
let mut conn = Connection::open(file.path()).unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, ?, ?, ?)",
params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
).unwrap();
create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
let db = MirageDb::open(file.path()).unwrap();
let status = db.status().unwrap();
assert_eq!(
status.cfg_blocks, 0,
"Empty database should have 0 cfg_blocks"
);
assert_eq!(
status.cfg_edges, 0,
"cfg_edges count should be 0 (table managed by Magellan)"
);
assert_eq!(
status.cfg_paths, 0,
"Empty database should have 0 cfg_paths"
);
assert_eq!(
status.cfg_dominators, 0,
"Empty database should have 0 cfg_dominators"
);
}
}
#[cfg(test)]
mod paths_tests {
use super::*;
use crate::cfg::{enumerate_paths, PathKind, PathLimits};
#[test]
fn test_paths_enumeration_basic() {
let cfg = cmds::create_test_cfg();
let limits = PathLimits::default();
let paths = enumerate_paths(&cfg, &limits);
assert!(!paths.is_empty(), "Should find at least one path");
assert_eq!(paths.len(), 2, "Test CFG should have exactly 2 paths");
let normal_count = paths.iter().filter(|p| p.kind == PathKind::Normal).count();
assert_eq!(normal_count, 2, "Both paths should be Normal");
}
#[test]
fn test_paths_show_errors_filter() {
let cfg = cmds::create_test_cfg();
let limits = PathLimits::default();
let mut paths = enumerate_paths(&cfg, &limits);
paths.retain(|p| p.kind == PathKind::Error);
assert_eq!(paths.len(), 0, "Test CFG should have no error paths");
for path in &paths {
assert_eq!(
path.kind,
PathKind::Error,
"Filtered paths should all be Error kind"
);
}
}
#[test]
fn test_paths_max_length_limit() {
let cfg = cmds::create_test_cfg();
let limits = PathLimits::default().with_max_length(1);
let paths = enumerate_paths(&cfg, &limits);
for path in &paths {
assert!(path.len() <= 1, "Path length should be <= max_length limit");
}
let unlimited_paths = enumerate_paths(&cfg, &PathLimits::default());
assert!(
paths.len() <= unlimited_paths.len(),
"Limited enumeration should produce <= paths than unlimited"
);
}
#[test]
fn test_paths_args_function_extraction() {
let args = PathsArgs {
function: "test_function".to_string(),
file: None,
show_errors: false,
max_length: None,
with_blocks: false,
incremental: false,
since: None,
by_coverage: false,
};
assert_eq!(args.function, "test_function");
assert!(!args.show_errors);
assert!(args.max_length.is_none());
assert!(!args.with_blocks);
}
#[test]
fn test_paths_args_with_flags() {
let args = PathsArgs {
function: "my_func".to_string(),
file: None,
show_errors: true,
max_length: Some(10),
with_blocks: true,
incremental: false,
since: None,
by_coverage: false,
};
assert_eq!(args.function, "my_func");
assert!(args.show_errors, "show_errors flag should be true");
assert_eq!(args.max_length, Some(10), "max_length should be Some(10)");
assert!(args.with_blocks, "with_blocks flag should be true");
}
#[test]
fn test_path_summary_from_path() {
use crate::cfg::Path;
let path = Path::new(vec![0, 1, 2], PathKind::Normal);
let summary = PathSummary::from(path);
assert!(!summary.path_id.is_empty(), "path_id should not be empty");
assert_eq!(summary.kind, "Normal", "kind should match PathKind");
assert_eq!(summary.length, 3, "length should match path length");
assert_eq!(summary.blocks.len(), 3, "should have 3 blocks");
assert_eq!(summary.blocks[0].block_id, 0, "first block_id should be 0");
assert_eq!(summary.blocks[1].block_id, 1, "second block_id should be 1");
assert_eq!(summary.blocks[2].block_id, 2, "third block_id should be 2");
assert_eq!(
summary.blocks[0].terminator, "Unknown",
"terminator should be Unknown placeholder"
);
assert!(summary.summary.is_none(), "summary should be None");
assert!(
summary.source_range.is_none(),
"source_range should be None"
);
}
#[test]
fn test_path_summary_different_kinds() {
use crate::cfg::Path;
let kinds = vec![
(PathKind::Normal, "Normal"),
(PathKind::Error, "Error"),
(PathKind::Degenerate, "Degenerate"),
(PathKind::Unreachable, "Unreachable"),
];
for (kind, expected_str) in kinds {
let path = Path::new(vec![0, 1], kind);
let summary = PathSummary::from(path);
assert_eq!(
summary.kind, expected_str,
"PathKind::{:?} should serialize to {}",
kind, expected_str
);
}
}
#[test]
fn test_paths_response_multiple_paths() {
use crate::cfg::Path;
let paths = vec![
Path::new(vec![0, 1], PathKind::Normal),
Path::new(vec![0, 2], PathKind::Normal),
Path::new(vec![0, 1, 3], PathKind::Error),
];
let summaries: Vec<PathSummary> = paths.into_iter().map(PathSummary::from).collect();
assert_eq!(summaries.len(), 3, "Should have 3 summaries");
let error_summaries = summaries.iter().filter(|s| s.kind == "Error").count();
assert_eq!(error_summaries, 1, "Should have 1 error path");
}
#[test]
fn test_paths_response_metadata() {
let response = PathsResponse {
function: "test_func".to_string(),
total_paths: 5,
error_paths: 2,
paths: vec![],
};
assert_eq!(response.function, "test_func");
assert_eq!(response.total_paths, 5);
assert_eq!(response.error_paths, 2);
assert!(response.paths.is_empty());
}
#[test]
fn test_paths_integration_with_test_cfg() {
let cfg = cmds::create_test_cfg();
let limits = PathLimits::default();
let paths = enumerate_paths(&cfg, &limits);
assert!(!paths.is_empty(), "Test CFG should produce paths");
for path in &paths {
assert_eq!(path.blocks[0], 0, "All paths should start at entry block 0");
assert_eq!(path.entry, 0, "Path entry should be block 0");
}
for path in &paths {
assert!(
path.exit == 2 || path.exit == 3,
"Path exit should be either block 2 or 3 (the return blocks)"
);
}
}
#[test]
fn test_paths_args_with_blocks_flag() {
let args_with = PathsArgs {
function: "test".to_string(),
file: None,
show_errors: false,
max_length: None,
with_blocks: true,
incremental: false,
since: None,
by_coverage: false,
};
let args_without = PathsArgs {
function: "test".to_string(),
file: None,
show_errors: false,
max_length: None,
with_blocks: false,
incremental: false,
since: None,
by_coverage: false,
};
assert!(args_with.with_blocks, "with_blocks should be true");
assert!(!args_without.with_blocks, "with_blocks should be false");
}
#[test]
fn test_path_summary_from_with_cfg() {
use crate::cfg::{
BasicBlock, BlockKind, EdgeType, Path, PathKind, SourceLocation, Terminator,
};
use petgraph::graph::DiGraph;
use std::path::PathBuf;
let mut g = DiGraph::new();
let loc0 = SourceLocation {
file_path: PathBuf::from("test.rs"),
byte_start: 0,
byte_end: 10,
start_line: 1,
start_column: 1,
end_line: 1,
end_column: 10,
};
let loc1 = SourceLocation {
file_path: PathBuf::from("test.rs"),
byte_start: 11,
byte_end: 20,
start_line: 2,
start_column: 1,
end_line: 2,
end_column: 10,
};
let loc2 = SourceLocation {
file_path: PathBuf::from("test.rs"),
byte_start: 21,
byte_end: 30,
start_line: 3,
start_column: 1,
end_line: 3,
end_column: 10,
};
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec!["let x = 1".to_string()],
terminator: Terminator::Goto { target: 1 },
source_location: Some(loc0),
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec!["if x > 0".to_string()],
terminator: Terminator::SwitchInt {
targets: vec![2],
otherwise: 2,
},
source_location: Some(loc1),
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec!["return true".to_string()],
terminator: Terminator::Return,
source_location: Some(loc2),
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::TrueBranch);
let path = Path::new(vec![0, 1, 2], PathKind::Normal);
let summary = PathSummary::from_with_cfg(path, &g);
assert_eq!(summary.blocks[0].terminator, "Goto { target: 1 }");
assert!(summary.blocks[1].terminator.contains("SwitchInt"));
assert_eq!(summary.blocks[2].terminator, "Return");
assert!(
summary.source_range.is_some(),
"source_range should be Some"
);
let sr = summary.source_range.as_ref().unwrap();
assert_eq!(sr.file_path, "test.rs");
assert_eq!(sr.start_line, 1);
assert_eq!(sr.end_line, 3);
}
#[test]
fn test_path_summary_from_with_cfg_no_source_locations() {
use crate::cfg::{Path, PathKind};
let cfg = cmds::create_test_cfg();
let path = Path::new(vec![0, 1, 2], PathKind::Normal);
let summary = PathSummary::from_with_cfg(path, &cfg);
assert!(summary.blocks[0].terminator.contains("Goto"));
assert!(summary.blocks[1].terminator.contains("SwitchInt"));
assert_eq!(summary.blocks[2].terminator, "Return");
assert!(
summary.source_range.is_none(),
"source_range should be None when CFG has no locations"
);
}
#[test]
fn test_paths_cache_miss_first_call() {
use crate::cfg::get_or_enumerate_paths;
use crate::storage::create_schema;
use rusqlite::Connection;
let mut conn = Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, 4, 3, 0)",
[],
).unwrap();
create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
let cfg = cmds::create_test_cfg();
let limits = PathLimits::default();
let test_function_id: i64 = 1; conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
rusqlite::params!("function", "test_func", "test.rs", "{}"),
)
.unwrap();
conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
let test_function_hash: &str = "test_cfg";
let paths1 = get_or_enumerate_paths(
&cfg,
test_function_id,
test_function_hash,
&limits,
&mut conn,
)
.unwrap();
assert!(
!paths1.is_empty(),
"First call should enumerate and return paths"
);
assert_eq!(paths1.len(), 2, "Test CFG should have 2 paths");
let path_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
rusqlite::params![test_function_id],
|row| row.get(0),
)
.unwrap();
assert_eq!(
path_count, 2,
"Paths should be stored in database after first call"
);
}
#[test]
fn test_paths_cache_hit_second_call() {
use crate::cfg::get_or_enumerate_paths;
use crate::storage::create_schema;
use rusqlite::Connection;
let mut conn = Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, 4, 3, 0)",
[],
).unwrap();
create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
rusqlite::params!("function", "test_func", "test.rs", "{}"),
)
.unwrap();
conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
let cfg = cmds::create_test_cfg();
let limits = PathLimits::default();
let test_function_id: i64 = 1; let test_function_hash: &str = "test_cfg";
let paths1 = get_or_enumerate_paths(
&cfg,
test_function_id,
test_function_hash,
&limits,
&mut conn,
)
.unwrap();
let path_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
rusqlite::params![test_function_id],
|row| row.get(0),
)
.unwrap();
assert_eq!(path_count, 2, "Should have 2 paths stored after first call");
let paths2 = get_or_enumerate_paths(
&cfg,
test_function_id,
test_function_hash,
&limits,
&mut conn,
)
.unwrap();
assert_eq!(
paths2.len(),
paths1.len(),
"Cache hit should return same number of paths"
);
let mut path_ids1: Vec<_> = paths1.iter().map(|p| &p.path_id).collect();
let mut path_ids2: Vec<_> = paths2.iter().map(|p| &p.path_id).collect();
path_ids1.sort();
path_ids2.sort();
assert_eq!(
path_ids1, path_ids2,
"Cache hit should return paths with same IDs"
);
for (p1, p2) in paths1.iter().zip(paths2.iter()) {
assert_eq!(p1.path_id, p2.path_id, "Path IDs should match on cache hit");
assert_eq!(p1.kind, p2.kind, "Path kinds should match on cache hit");
assert_eq!(
p1.blocks, p2.blocks,
"Path blocks should match on cache hit"
);
}
}
#[test]
fn test_paths_cache_invalidation_on_hash_change() {
use crate::cfg::get_or_enumerate_paths;
use crate::storage::create_schema;
use rusqlite::Connection;
let mut conn = Connection::open_in_memory().unwrap();
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)
.unwrap();
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, 4, 3, 0)",
[],
).unwrap();
create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
rusqlite::params!("function", "test_func", "test.rs", "{}"),
)
.unwrap();
conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
let cfg = cmds::create_test_cfg();
let limits = PathLimits::default();
let test_function_id: i64 = 1; let test_function_hash_v1: &str = "test_cfg_v1";
let test_function_hash_v3: &str = "test_cfg_v3";
let paths1 = get_or_enumerate_paths(
&cfg,
test_function_id,
test_function_hash_v1,
&limits,
&mut conn,
)
.unwrap();
let path_count_v1: i64 = conn
.query_row(
"SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
rusqlite::params![test_function_id],
|row| row.get(0),
)
.unwrap();
assert_eq!(path_count_v1, 2, "Should have 2 paths after first call");
let paths2 = get_or_enumerate_paths(
&cfg,
test_function_id,
test_function_hash_v3,
&limits,
&mut conn,
)
.unwrap();
assert!(!paths2.is_empty(), "Should re-enumerate");
assert_eq!(
paths2.len(),
paths1.len(),
"Re-enumeration should produce same paths"
);
let path_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
rusqlite::params![test_function_id],
|row| row.get(0),
)
.unwrap();
assert_eq!(path_count, 2, "Should have 2 paths after re-enumeration");
}
}
#[cfg(test)]
mod unreachable_tests {
use super::*;
use crate::cfg::reachability::find_unreachable;
use crate::cfg::{BasicBlock, BlockKind, Cfg, EdgeType, Terminator};
use petgraph::graph::DiGraph;
fn create_cfg_with_unreachable() -> Cfg {
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec!["let x = 1".to_string()],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec!["if x > 0".to_string()],
terminator: Terminator::SwitchInt {
targets: vec![2],
otherwise: 3,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec!["return true".to_string()],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec!["return false".to_string()],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let _b4 = g.add_node(BasicBlock {
id: 4,
db_id: None,
kind: BlockKind::Exit,
statements: vec!["unreachable code".to_string()],
terminator: Terminator::Unreachable,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::TrueBranch);
g.add_edge(b1, b3, EdgeType::FalseBranch);
g
}
#[test]
fn test_unreachable_detects_dead_code() {
let cfg = create_cfg_with_unreachable();
let unreachable_indices = find_unreachable(&cfg);
assert_eq!(
unreachable_indices.len(),
1,
"Should find exactly 1 unreachable block"
);
let block_id = cfg.node_weight(unreachable_indices[0]).unwrap().id;
assert_eq!(block_id, 4, "Unreachable block should be block 4");
}
#[test]
fn test_unreachable_response_serialization() {
use crate::output::JsonResponse;
let response = UnreachableResponse {
uncalled_functions: None,
function: "test_func".to_string(),
total_functions: 1,
functions_with_unreachable: 1,
unreachable_count: 1,
blocks: vec![UnreachableBlock {
block_id: 4,
kind: "Exit".to_string(),
statements: vec!["unreachable code".to_string()],
terminator: "Unreachable".to_string(),
incoming_edges: vec![],
}],
};
let wrapper = JsonResponse::new(response);
let json = wrapper.to_json();
assert!(json.contains("\"function\":\"test_func\""));
assert!(json.contains("\"unreachable_count\":1"));
assert!(json.contains("\"block_id\":4"));
assert!(json.contains("\"kind\":\"Exit\""));
}
#[test]
fn test_unreachable_empty_response() {
use crate::output::JsonResponse;
let response = UnreachableResponse {
uncalled_functions: None,
function: "test_func".to_string(),
total_functions: 1,
functions_with_unreachable: 0,
unreachable_count: 0,
blocks: vec![],
};
let wrapper = JsonResponse::new(response);
let json = wrapper.to_json();
assert!(json.contains("\"unreachable_count\":0"));
assert!(json.contains("\"functions_with_unreachable\":0"));
}
#[test]
fn test_unreachable_block_fields() {
let block = UnreachableBlock {
block_id: 5,
kind: "Normal".to_string(),
statements: vec!["stmt1".to_string(), "stmt2".to_string()],
terminator: "Return".to_string(),
incoming_edges: vec![],
};
assert_eq!(block.block_id, 5);
assert_eq!(block.kind, "Normal");
assert_eq!(block.statements.len(), 2);
assert_eq!(block.terminator, "Return");
}
#[test]
fn test_unreachable_args_flags() {
let args_with = UnreachableArgs {
include_uncalled: false,
within_functions: true,
show_branches: true,
};
let args_without = UnreachableArgs {
include_uncalled: false,
within_functions: false,
show_branches: false,
};
assert!(args_with.within_functions);
assert!(args_with.show_branches);
assert!(!args_without.within_functions);
assert!(!args_without.show_branches);
}
#[test]
fn test_test_cfg_fully_reachable() {
let cfg = cmds::create_test_cfg();
let unreachable_indices = find_unreachable(&cfg);
assert_eq!(
unreachable_indices.len(),
0,
"Test CFG should have no unreachable blocks"
);
}
#[test]
fn test_unreachable_show_branches_with_edges() {
use crate::cfg::reachability::find_unreachable;
use petgraph::visit::EdgeRef;
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec!["let x = 1".to_string()],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec!["if x > 0".to_string()],
terminator: Terminator::SwitchInt {
targets: vec![2],
otherwise: 3,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec!["return true".to_string()],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Normal,
statements: vec!["unreachable branch".to_string()],
terminator: Terminator::Goto { target: 4 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b4 = g.add_node(BasicBlock {
id: 4,
db_id: None,
kind: BlockKind::Exit,
statements: vec!["unreachable code".to_string()],
terminator: Terminator::Unreachable,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::TrueBranch);
g.add_edge(b3, b4, EdgeType::Fallthrough);
let unreachable_indices = find_unreachable(&g);
let blocks: Vec<UnreachableBlock> = unreachable_indices
.iter()
.map(|&idx| {
let block = &g[idx];
let kind_str = format!("{:?}", block.kind);
let terminator_str = format!("{:?}", block.terminator);
let incoming_edges: Vec<IncomingEdge> = g
.edge_references()
.filter(|edge| edge.target() == idx)
.map(|edge| {
let source_block = &g[edge.source()];
let edge_type = g.edge_weight(edge.id()).unwrap();
IncomingEdge {
from_block: source_block.id,
edge_type: format!("{:?}", edge_type),
}
})
.collect();
UnreachableBlock {
block_id: block.id,
kind: kind_str,
statements: block.statements.clone(),
terminator: terminator_str,
incoming_edges,
}
})
.collect();
assert_eq!(blocks.len(), 2);
let block3 = blocks.iter().find(|b| b.block_id == 3).unwrap();
assert_eq!(block3.incoming_edges.len(), 0);
let block4 = blocks.iter().find(|b| b.block_id == 4).unwrap();
assert_eq!(block4.incoming_edges.len(), 1);
assert_eq!(block4.incoming_edges[0].from_block, 3);
assert_eq!(block4.incoming_edges[0].edge_type, "Fallthrough");
}
#[test]
fn test_unreachable_show_branches_json_output() {
use crate::cfg::reachability::find_unreachable;
use crate::output::JsonResponse;
use petgraph::visit::EdgeRef;
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec!["let x = 1".to_string()],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec!["if x > 0".to_string()],
terminator: Terminator::SwitchInt {
targets: vec![2],
otherwise: 3,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Exit,
statements: vec!["return true".to_string()],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Normal,
statements: vec!["unreachable branch".to_string()],
terminator: Terminator::Goto { target: 4 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b4 = g.add_node(BasicBlock {
id: 4,
db_id: None,
kind: BlockKind::Exit,
statements: vec!["unreachable code".to_string()],
terminator: Terminator::Unreachable,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::TrueBranch);
g.add_edge(b3, b4, EdgeType::Fallthrough);
let unreachable_indices = find_unreachable(&g);
let blocks: Vec<UnreachableBlock> = unreachable_indices
.iter()
.map(|&idx| {
let block = &g[idx];
UnreachableBlock {
block_id: block.id,
kind: format!("{:?}", block.kind),
statements: block.statements.clone(),
terminator: format!("{:?}", block.terminator),
incoming_edges: g
.edge_references()
.filter(|edge| edge.target() == idx)
.map(|edge| {
let source_block = &g[edge.source()];
let edge_type = g.edge_weight(edge.id()).unwrap();
IncomingEdge {
from_block: source_block.id,
edge_type: format!("{:?}", edge_type),
}
})
.collect(),
}
})
.collect();
let response = UnreachableResponse {
function: "test".to_string(),
total_functions: 1,
functions_with_unreachable: 1,
unreachable_count: 2,
blocks,
uncalled_functions: None,
};
let wrapper = JsonResponse::new(response);
let json = wrapper.to_json();
assert!(json.contains("\"incoming_edges\""));
assert!(json.contains("\"from_block\":3"));
assert!(json.contains("\"edge_type\":\"Fallthrough\""));
}
#[test]
fn test_incoming_edge_serialization() {
let edge = IncomingEdge {
from_block: 5,
edge_type: "TrueBranch".to_string(),
};
let serialized = serde_json::to_string(&edge).unwrap();
assert!(serialized.contains("\"from_block\":5"));
assert!(serialized.contains("\"edge_type\":\"TrueBranch\""));
}
}
#[cfg(test)]
mod dominators_tests {
use super::*;
use crate::cfg::{DominatorTree, PostDominatorTree};
use tempfile::NamedTempFile;
fn create_minimal_db() -> anyhow::Result<NamedTempFile> {
use crate::storage::{
REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
};
let file = NamedTempFile::new()?;
let conn = rusqlite::Connection::open(file.path())?;
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)?;
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)?;
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, ?, ?, ?)",
rusqlite::params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
)?;
conn.execute(
"CREATE TABLE mirage_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
mirage_schema_version INTEGER NOT NULL,
magellan_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)?;
conn.execute(
"CREATE TABLE cfg_blocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
function_id INTEGER NOT NULL,
kind TEXT NOT NULL,
byte_start INTEGER NOT NULL,
byte_end INTEGER NOT NULL,
terminator TEXT NOT NULL,
function_hash TEXT NOT NULL
)",
[],
)?;
conn.execute(
"CREATE TABLE cfg_paths (
path_id TEXT PRIMARY KEY,
function_id INTEGER NOT NULL,
path_kind TEXT NOT NULL,
entry_block INTEGER NOT NULL,
exit_block INTEGER NOT NULL,
length INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)?;
conn.execute(
"CREATE TABLE cfg_dominators (
id INTEGER PRIMARY KEY AUTOINCREMENT,
block_id INTEGER NOT NULL,
dominator_id INTEGER NOT NULL,
is_strict INTEGER NOT NULL
)",
[],
)?;
conn.execute(
"INSERT INTO mirage_meta (id, mirage_schema_version, magellan_schema_version, created_at)
VALUES (1, 1, 4, 0)",
[],
)?;
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
rusqlite::params!("function", "test_func", "test.rs", "{}"),
)?;
Ok(file)
}
#[test]
fn test_dominator_tree_computation() {
let cfg = cmds::create_test_cfg();
let dom_tree = DominatorTree::new(&cfg);
assert!(
dom_tree.is_some(),
"DominatorTree should be computed successfully"
);
let dom_tree = dom_tree.unwrap();
assert_eq!(cfg[dom_tree.root()].id, 0, "Root should be entry block");
}
#[test]
fn test_post_dominator_tree_computation() {
let cfg = cmds::create_test_cfg();
let post_dom_tree = PostDominatorTree::new(&cfg);
assert!(
post_dom_tree.is_some(),
"PostDominatorTree should be computed successfully"
);
let post_dom_tree = post_dom_tree.unwrap();
let root_id = cfg[post_dom_tree.root()].id;
assert!(root_id == 2 || root_id == 3, "Root should be an exit block");
}
#[test]
fn test_immediate_dominator_relationships() {
let cfg = cmds::create_test_cfg();
let dom_tree = DominatorTree::new(&cfg).unwrap();
let node_0 = cfg.node_indices().find(|&n| cfg[n].id == 0).unwrap();
let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
let node_2 = cfg.node_indices().find(|&n| cfg[n].id == 2).unwrap();
let node_3 = cfg.node_indices().find(|&n| cfg[n].id == 3).unwrap();
assert_eq!(
dom_tree.immediate_dominator(node_0),
None,
"Entry should have no dominator"
);
assert_eq!(
dom_tree.immediate_dominator(node_1),
Some(node_0),
"Node 1 should be dominated by entry"
);
assert_eq!(
dom_tree.immediate_dominator(node_2),
Some(node_1),
"Node 2 should be dominated by node 1"
);
assert_eq!(
dom_tree.immediate_dominator(node_3),
Some(node_1),
"Node 3 should be dominated by node 1"
);
}
#[test]
fn test_dominates_method() {
let cfg = cmds::create_test_cfg();
let dom_tree = DominatorTree::new(&cfg).unwrap();
let node_0 = cfg.node_indices().find(|&n| cfg[n].id == 0).unwrap();
let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
let node_2 = cfg.node_indices().find(|&n| cfg[n].id == 2).unwrap();
assert!(dom_tree.dominates(node_0, node_0), "Node dominates itself");
assert!(dom_tree.dominates(node_0, node_1), "Entry dominates node 1");
assert!(dom_tree.dominates(node_0, node_2), "Entry dominates node 2");
assert!(
!dom_tree.dominates(node_1, node_0),
"Node 1 does not dominate entry"
);
}
#[test]
fn test_dominator_tree_children() {
let cfg = cmds::create_test_cfg();
let dom_tree = DominatorTree::new(&cfg).unwrap();
let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
let children = dom_tree.children(node_1);
assert_eq!(children.len(), 2, "Node 1 should have 2 children");
let child_ids: Vec<_> = children.iter().map(|&n| cfg[n].id).collect();
assert!(child_ids.contains(&2), "Children should include block 2");
assert!(child_ids.contains(&3), "Children should include block 3");
}
#[test]
fn test_dominators_args_fields() {
let args = DominatorsArgs {
function: "test_func".to_string(),
file: None,
must_pass_through: Some("1".to_string()),
post: false,
inter_procedural: false,
};
assert_eq!(args.function, "test_func");
assert_eq!(args.must_pass_through, Some("1".to_string()));
assert!(!args.post);
assert!(!args.inter_procedural);
}
#[test]
fn test_dominators_args_with_post_flag() {
let args = DominatorsArgs {
function: "my_function".to_string(),
file: None,
must_pass_through: None,
post: true,
inter_procedural: false,
};
assert_eq!(args.function, "my_function");
assert!(args.post, "post flag should be true");
assert!(
args.must_pass_through.is_none(),
"must_pass_through should be None"
);
assert!(!args.inter_procedural);
}
#[test]
fn test_dominance_response_serialization() {
let response = DominanceResponse {
function: "test".to_string(),
kind: "dominators".to_string(),
root: Some(0),
dominance_tree: vec![DominatorEntry {
block: 0,
immediate_dominator: None,
dominated: vec![1],
}],
must_pass_through: None,
};
let json = serde_json::to_string(&response);
assert!(json.is_ok(), "DominanceResponse should serialize to JSON");
let json_str = json.unwrap();
assert!(json_str.contains("\"function\":\"test\""));
assert!(json_str.contains("\"kind\":\"dominators\""));
assert!(json_str.contains("\"root\":0"));
}
#[test]
fn test_must_pass_through_result() {
let result = MustPassThroughResult {
block: 1,
must_pass: vec![1, 2, 3],
};
assert_eq!(result.block, 1);
assert_eq!(result.must_pass.len(), 3);
assert_eq!(result.must_pass, vec![1, 2, 3]);
let json = serde_json::to_string(&result);
assert!(json.is_ok());
let json_str = json.unwrap();
assert!(json_str.contains("\"block\":1"));
assert!(json_str.contains("\"must_pass\":[1,2,3]"));
}
#[test]
fn test_dominator_entry() {
let entry = DominatorEntry {
block: 5,
immediate_dominator: Some(2),
dominated: vec![6, 7],
};
assert_eq!(entry.block, 5);
assert_eq!(entry.immediate_dominator, Some(2));
assert_eq!(entry.dominated, vec![6, 7]);
}
#[test]
fn test_post_dominates_method() {
let cfg = cmds::create_test_cfg();
let post_dom_tree = PostDominatorTree::new(&cfg).unwrap();
let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
let node_2 = cfg.node_indices().find(|&n| cfg[n].id == 2).unwrap();
assert!(
post_dom_tree.post_dominates(node_2, node_2),
"Node post-dominates itself"
);
assert!(
post_dom_tree.post_dominates(node_2, node_1),
"Exit post-dominates node 1"
);
}
#[test]
fn test_immediate_post_dominator_relationships() {
let cfg = cmds::create_test_cfg();
let post_dom_tree = PostDominatorTree::new(&cfg).unwrap();
let node_0 = cfg.node_indices().find(|&n| cfg[n].id == 0).unwrap();
let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
let ipdom_1 = post_dom_tree.immediate_post_dominator(node_1);
assert!(
ipdom_1.is_some(),
"Node 1 should have an immediate post-dominator"
);
let ipdom_0 = post_dom_tree.immediate_post_dominator(node_0);
assert_eq!(
ipdom_0,
Some(node_1),
"Node 0 should be immediately post-dominated by node 1"
);
}
#[test]
fn test_empty_cfg_dominator_tree() {
use petgraph::graph::DiGraph;
let empty_cfg: crate::cfg::Cfg = DiGraph::new();
let dom_tree = DominatorTree::new(&empty_cfg);
assert!(
dom_tree.is_none(),
"Empty CFG should produce None for DominatorTree"
);
}
#[test]
fn test_empty_cfg_post_dominator_tree() {
use petgraph::graph::DiGraph;
let empty_cfg: crate::cfg::Cfg = DiGraph::new();
let post_dom_tree = PostDominatorTree::new(&empty_cfg);
assert!(
post_dom_tree.is_none(),
"Empty CFG should produce None for PostDominatorTree"
);
}
#[test]
fn test_dominance_response_json_wrapper() {
use crate::output::JsonResponse;
let response = DominanceResponse {
function: "wrapped_test".to_string(),
kind: "dominators".to_string(),
root: Some(0),
dominance_tree: vec![],
must_pass_through: None,
};
let wrapper = JsonResponse::new(response);
assert_eq!(wrapper.schema_version, "1.0.1");
assert_eq!(wrapper.tool, "mirage");
assert!(!wrapper.execution_id.is_empty());
assert!(!wrapper.timestamp.is_empty());
let json = wrapper.to_json();
assert!(json.contains("\"schema_version\":\"1.0.1\""));
assert!(json.contains("\"tool\":\"mirage\""));
assert!(json.contains("wrapped_test"));
}
#[test]
fn test_must_pass_through_valid_block() {
let cfg = cmds::create_test_cfg();
let dom_tree = DominatorTree::new(&cfg).unwrap();
let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
let must_pass: Vec<usize> = cfg
.node_indices()
.filter(|&n| dom_tree.dominates(node_1, n))
.map(|n| cfg[n].id)
.collect();
assert_eq!(must_pass.len(), 3, "Block 1 should dominate 3 blocks");
assert!(must_pass.contains(&1), "Must include block 1 itself");
assert!(must_pass.contains(&2), "Must include block 2");
assert!(must_pass.contains(&3), "Must include block 3");
}
#[test]
fn test_nonexistent_block_id() {
let cfg = cmds::create_test_cfg();
let found = cfg.node_indices().find(|&n| cfg[n].id == 99);
assert!(found.is_none(), "Non-existent block should not be found");
}
#[test]
fn test_dominators_json_structure() {
use crate::output::JsonResponse;
let response = DominanceResponse {
function: "json_test".to_string(),
kind: "post-dominators".to_string(),
root: Some(3),
dominance_tree: vec![DominatorEntry {
block: 3,
immediate_dominator: None,
dominated: vec![0, 2],
}],
must_pass_through: Some(MustPassThroughResult {
block: 0,
must_pass: vec![0, 1],
}),
};
let wrapper = JsonResponse::new(response);
let json = wrapper.to_json();
assert!(json.contains("\"kind\":\"post-dominators\""));
assert!(json.contains("\"root\":3"));
assert!(json.contains("\"must_pass_through\""));
assert!(json.contains("\"block\":0"));
}
}
#[cfg(test)]
mod verify_tests {
use super::*;
use crate::cfg::{enumerate_paths, PathLimits};
use crate::output::JsonResponse;
use crate::storage::MirageDb;
fn create_test_db_with_cached_path(
) -> anyhow::Result<(tempfile::NamedTempFile, MirageDb, String)> {
use crate::storage::{
REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
};
let file = tempfile::NamedTempFile::new()?;
let mut conn = rusqlite::Connection::open(file.path())?;
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)?;
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
)",
[],
)?;
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, ?, ?, ?)",
rusqlite::params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
)?;
crate::storage::create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION)?;
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
rusqlite::params!("function", "test_func", "test.rs", "{}"),
)?;
let function_id: i64 = conn.last_insert_rowid();
let cfg = cmds::create_test_cfg();
let paths = enumerate_paths(&cfg, &PathLimits::default());
if let Some(first_path) = paths.first() {
let path_id = &first_path.path_id;
conn.execute(
"INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
rusqlite::params![
path_id,
function_id,
"Normal",
first_path.entry as i64,
first_path.exit as i64,
first_path.len() as i64,
0,
],
)?;
for (idx, &block_id) in first_path.blocks.iter().enumerate() {
conn.execute(
"INSERT INTO cfg_path_elements (path_id, sequence_order, block_id)
VALUES (?1, ?2, ?3)",
rusqlite::params![path_id, idx as i64, block_id as i64],
)?;
}
let db = MirageDb::open(file.path())?;
Ok((file, db, path_id.clone()))
} else {
anyhow::bail!("No paths found in test CFG")
}
}
#[test]
#[cfg(feature = "backend-sqlite")]
fn test_verify_valid_path() {
let (_file, _db, cached_path_id) = create_test_db_with_cached_path().unwrap();
let cfg = cmds::create_test_cfg();
let current_paths = enumerate_paths(&cfg, &PathLimits::default());
let is_valid = current_paths.iter().any(|p| p.path_id == cached_path_id);
assert!(is_valid, "Cached path should exist in current enumeration");
}
#[test]
fn test_verify_result_serialization() {
let result = VerifyResult {
path_id: "test_path_123".to_string(),
valid: true,
found_in_cache: true,
function_id: Some(1),
reason: "Path found in current enumeration".to_string(),
current_paths: 2,
};
let json = serde_json::to_string(&result);
assert!(json.is_ok());
let json_str = json.unwrap();
assert!(json_str.contains("\"path_id\":\"test_path_123\""));
assert!(json_str.contains("\"valid\":true"));
assert!(json_str.contains("\"found_in_cache\":true"));
assert!(json_str.contains("\"function_id\":1"));
assert!(json_str.contains("\"reason\""));
assert!(json_str.contains("\"current_paths\":2"));
}
#[test]
fn test_verify_invalid_path_result() {
let result = VerifyResult {
path_id: "nonexistent_path".to_string(),
valid: false,
found_in_cache: false,
function_id: None,
reason: "Path not found in cache".to_string(),
current_paths: 0,
};
assert!(!result.valid);
assert!(!result.found_in_cache);
assert!(result.function_id.is_none());
assert_eq!(result.reason, "Path not found in cache");
}
#[test]
fn test_verify_args_fields() {
let args = VerifyArgs {
path_id: "abc123".to_string(),
};
assert_eq!(args.path_id, "abc123");
}
#[test]
fn test_verify_result_json_wrapper() {
let result = VerifyResult {
path_id: "wrapped_path".to_string(),
valid: true,
found_in_cache: true,
function_id: Some(42),
reason: "Test reason".to_string(),
current_paths: 100,
};
let wrapper = JsonResponse::new(result);
assert_eq!(wrapper.schema_version, "1.0.1");
assert_eq!(wrapper.tool, "mirage");
assert!(!wrapper.execution_id.is_empty());
assert!(!wrapper.timestamp.is_empty());
let json = wrapper.to_json();
assert!(json.contains("\"schema_version\":\"1.0.1\""));
assert!(json.contains("\"tool\":\"mirage\""));
assert!(json.contains("wrapped_path"));
}
#[test]
fn test_verify_check_path_exists() {
let cfg = cmds::create_test_cfg();
let paths = enumerate_paths(&cfg, &PathLimits::default());
if let Some(first_path) = paths.first() {
let path_id = &first_path.path_id;
let exists = paths.iter().any(|p| &p.path_id == path_id);
assert!(exists, "Path should exist in enumeration");
let same_blocks = paths.iter().any(|p| p.blocks == first_path.blocks);
assert!(same_blocks, "Should find path with same blocks");
}
}
#[test]
fn test_verify_multiple_paths_have_different_ids() {
let cfg = cmds::create_test_cfg();
let paths = enumerate_paths(&cfg, &PathLimits::default());
assert!(paths.len() >= 2, "Test CFG should have at least 2 paths");
let mut path_ids = std::collections::HashSet::new();
for path in &paths {
assert!(
path_ids.insert(&path.path_id),
"Path ID should be unique: {}",
path.path_id
);
}
}
#[test]
fn test_verify_path_not_in_cache() {
let result = VerifyResult {
path_id: "fake_id_that_does_not_exist".to_string(),
valid: false,
found_in_cache: false,
function_id: None,
reason: "Path not found in cache".to_string(),
current_paths: 0,
};
assert!(!result.found_in_cache);
assert!(!result.valid);
}
#[test]
fn test_verify_json_output_format() {
let result = VerifyResult {
path_id: "json_test_path".to_string(),
valid: true,
found_in_cache: true,
function_id: Some(123),
reason: "Test".to_string(),
current_paths: 5,
};
let wrapper = JsonResponse::new(result);
let json = wrapper.to_pretty_json();
assert!(json.contains("\n"));
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["tool"], "mirage");
assert_eq!(parsed["data"]["path_id"], "json_test_path");
assert_eq!(parsed["data"]["valid"], true);
}
#[test]
fn test_verify_result_without_function_id() {
let result = VerifyResult {
path_id: "orphan_path".to_string(),
valid: false,
found_in_cache: false,
function_id: None,
reason: "No function associated".to_string(),
current_paths: 10,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"function_id\":null"));
assert!(!result.valid);
assert!(!result.found_in_cache);
}
}
#[cfg(test)]
mod output_format_tests {
use super::*;
use crate::output::JsonResponse;
#[test]
fn test_all_response_types_serialize() {
let paths_resp = PathsResponse {
function: "test_func".to_string(),
total_paths: 2,
error_paths: 0,
paths: vec![],
};
let paths_json = serde_json::to_string(&paths_resp);
assert!(paths_json.is_ok(), "PathsResponse should serialize");
let dom_resp = DominanceResponse {
function: "test_func".to_string(),
kind: "dominators".to_string(),
root: Some(0),
dominance_tree: vec![],
must_pass_through: None,
};
let dom_json = serde_json::to_string(&dom_resp);
assert!(dom_json.is_ok(), "DominanceResponse should serialize");
let unreach_resp = UnreachableResponse {
uncalled_functions: None,
function: "test_func".to_string(),
total_functions: 1,
functions_with_unreachable: 0,
unreachable_count: 0,
blocks: vec![],
};
let unreach_json = serde_json::to_string(&unreach_resp);
assert!(unreach_json.is_ok(), "UnreachableResponse should serialize");
let verify_res = VerifyResult {
path_id: "test_path".to_string(),
valid: true,
found_in_cache: true,
function_id: Some(1),
reason: "Test".to_string(),
current_paths: 2,
};
let verify_json = serde_json::to_string(&verify_res);
assert!(verify_json.is_ok(), "VerifyResult should serialize");
}
#[test]
fn test_json_response_wrapper_for_all_commands() {
let paths_resp = PathsResponse {
function: "test_func".to_string(),
total_paths: 2,
error_paths: 0,
paths: vec![],
};
let paths_wrapper = JsonResponse::new(paths_resp);
assert_eq!(paths_wrapper.schema_version, "1.0.1");
assert_eq!(paths_wrapper.tool, "mirage");
assert!(!paths_wrapper.execution_id.is_empty());
let dom_resp = DominanceResponse {
function: "test_func".to_string(),
kind: "dominators".to_string(),
root: Some(0),
dominance_tree: vec![],
must_pass_through: None,
};
let dom_wrapper = JsonResponse::new(dom_resp);
assert_eq!(dom_wrapper.schema_version, "1.0.1");
assert_eq!(dom_wrapper.tool, "mirage");
let unreach_resp = UnreachableResponse {
uncalled_functions: None,
function: "test_func".to_string(),
total_functions: 1,
functions_with_unreachable: 0,
unreachable_count: 0,
blocks: vec![],
};
let unreach_wrapper = JsonResponse::new(unreach_resp);
assert_eq!(unreach_wrapper.schema_version, "1.0.1");
assert_eq!(unreach_wrapper.tool, "mirage");
let verify_res = VerifyResult {
path_id: "test_path".to_string(),
valid: true,
found_in_cache: true,
function_id: Some(1),
reason: "Test".to_string(),
current_paths: 2,
};
let verify_wrapper = JsonResponse::new(verify_res);
assert_eq!(verify_wrapper.schema_version, "1.0.1");
assert_eq!(verify_wrapper.tool, "mirage");
}
#[test]
fn test_json_response_compact_format() {
let data = vec!["item1", "item2"];
let wrapper = JsonResponse::new(data);
let compact = wrapper.to_json();
assert!(
!compact.contains("\n"),
"Compact JSON should not have newlines"
);
assert!(
compact.contains("\"item1\""),
"Compact JSON should contain data"
);
}
#[test]
fn test_json_response_pretty_format() {
let data = vec!["item1", "item2"];
let wrapper = JsonResponse::new(data);
let pretty = wrapper.to_pretty_json();
assert!(pretty.contains("\n"), "Pretty JSON should have newlines");
assert!(pretty.contains(" "), "Pretty JSON should have indentation");
let compact = wrapper.to_json();
let compact_val: serde_json::Value = serde_json::from_str(&compact).unwrap();
let pretty_val: serde_json::Value = serde_json::from_str(&pretty).unwrap();
assert_eq!(
compact_val, pretty_val,
"Both formats should produce same data"
);
}
#[test]
fn test_json_response_required_fields() {
let data = "test_data";
let wrapper = JsonResponse::new(data);
assert_eq!(wrapper.schema_version, "1.0.1");
assert_eq!(wrapper.tool, "mirage");
assert!(!wrapper.execution_id.is_empty());
assert!(!wrapper.timestamp.is_empty());
assert!(
wrapper.execution_id.contains("-"),
"execution_id should contain hyphen"
);
let parsed_time = chrono::DateTime::parse_from_rfc3339(&wrapper.timestamp);
assert!(parsed_time.is_ok(), "timestamp should be valid RFC3339");
}
#[test]
fn test_output_format_enum_matches() {
assert_ne!(OutputFormat::Human, OutputFormat::Json);
assert_ne!(OutputFormat::Human, OutputFormat::Pretty);
assert_ne!(OutputFormat::Json, OutputFormat::Pretty);
assert_eq!(OutputFormat::Human, OutputFormat::Human);
assert_eq!(OutputFormat::Json, OutputFormat::Json);
assert_eq!(OutputFormat::Pretty, OutputFormat::Pretty);
}
#[test]
fn test_human_output_no_json_artifacts() {
let function_name = "test_function";
let path_count = 5;
let mut output = String::new();
output.push_str(&format!("Function: {}\n", function_name));
output.push_str(&format!("Total paths: {}\n", path_count));
assert!(
!output.contains("{"),
"Human output should not contain JSON objects"
);
assert!(
!output.contains("}"),
"Human output should not contain JSON objects"
);
assert!(
!output.contains("\""),
"Human output should not contain JSON quotes"
);
assert!(
!output.contains("schema_version"),
"Human output should not contain JSON metadata"
);
}
#[test]
fn test_json_output_has_metadata() {
let data = "test_data";
let wrapper = JsonResponse::new(data);
let json = wrapper.to_json();
assert!(json.contains("\"schema_version\""));
assert!(json.contains("\"execution_id\""));
assert!(json.contains("\"tool\""));
assert!(json.contains("\"timestamp\""));
assert!(json.contains("\"data\""));
}
#[test]
fn test_error_response_format() {
use crate::output::JsonError;
let error = JsonError::new("category", "message", "CODE");
assert_eq!(error.error, "category");
assert_eq!(error.message, "message");
assert_eq!(error.code, "CODE");
assert!(error.remediation.is_none());
let error_with_remediation = error.with_remediation("Try X instead");
assert_eq!(
error_with_remediation.remediation,
Some("Try X instead".to_string())
);
let json = serde_json::to_string(&error_with_remediation);
assert!(json.is_ok());
let json_str = json.unwrap();
assert!(json_str.contains("\"error\""));
assert!(json_str.contains("\"message\""));
assert!(json_str.contains("\"code\""));
assert!(json_str.contains("\"remediation\""));
}
#[test]
fn test_cli_with_different_output_formats() {
let formats = vec![
OutputFormat::Human,
OutputFormat::Json,
OutputFormat::Pretty,
];
for format in formats {
let cli = Cli {
db: Some("./test.db".to_string()),
output: format,
command: Some(Commands::Status(StatusArgs {})),
detect_backend: false,
};
assert_eq!(cli.output, format);
assert_eq!(cli.db, Some("./test.db".to_string()));
}
}
#[test]
fn test_cfg_format_enum() {
let formats = vec![CfgFormat::Human, CfgFormat::Dot, CfgFormat::Json];
for format in &formats {
match format {
CfgFormat::Human => assert!(true),
CfgFormat::Dot => assert!(true),
CfgFormat::Json => assert!(true),
}
}
assert_ne!(CfgFormat::Human, CfgFormat::Dot);
assert_ne!(CfgFormat::Human, CfgFormat::Json);
assert_ne!(CfgFormat::Dot, CfgFormat::Json);
}
#[test]
fn test_response_snake_case_naming() {
let paths_resp = PathsResponse {
function: "test".to_string(),
total_paths: 1,
error_paths: 0,
paths: vec![],
};
let json = serde_json::to_string(&paths_resp).unwrap();
assert!(json.contains("\"function\""));
assert!(json.contains("\"total_paths\""));
assert!(json.contains("\"error_paths\""));
assert!(!json.contains("\"totalPaths\""));
assert!(!json.contains("\"errorPaths\""));
}
#[test]
fn test_loops_detects_loops() {
use crate::cfg::{detect_natural_loops, BasicBlock, BlockKind, EdgeType, Terminator};
use petgraph::graph::DiGraph;
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![2],
otherwise: 3,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec!["loop body".to_string()],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::TrueBranch);
g.add_edge(b1, b3, EdgeType::FalseBranch);
g.add_edge(b2, b1, EdgeType::LoopBack);
let loops = detect_natural_loops(&g);
assert_eq!(loops.len(), 1, "Should detect exactly one loop");
assert_eq!(loops[0].header.index(), 1, "Loop header should be block 1");
}
#[test]
fn test_loops_empty_cfg() {
use crate::cfg::detect_natural_loops;
use petgraph::graph::DiGraph;
let empty_cfg: crate::cfg::Cfg = DiGraph::new();
let loops = detect_natural_loops(&empty_cfg);
assert!(loops.is_empty(), "Empty CFG should have no loops");
}
#[test]
fn test_loops_response_serialization() {
use crate::output::JsonResponse;
let response = LoopsResponse {
function: "test_func".to_string(),
loop_count: 2,
loops: vec![
LoopInfo {
header: 1,
back_edge_from: 2,
body_size: 2,
nesting_level: 0,
body_blocks: vec![1, 2],
},
LoopInfo {
header: 3,
back_edge_from: 4,
body_size: 3,
nesting_level: 1,
body_blocks: vec![1, 2, 3],
},
],
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"function\""));
assert!(json.contains("\"loop_count\""));
assert!(json.contains("\"loops\""));
let wrapper = JsonResponse::new(response);
let wrapped_json = wrapper.to_json();
assert!(wrapped_json.contains("\"schema_version\""));
assert!(wrapped_json.contains("\"execution_id\""));
}
#[test]
fn test_loops_args_fields() {
let args = LoopsArgs {
function: "my_function".to_string(),
file: None,
verbose: true,
};
assert_eq!(args.function, "my_function");
assert!(args.verbose);
}
#[test]
fn test_loop_info_fields() {
let loop_info = LoopInfo {
header: 5,
back_edge_from: 7,
body_size: 3,
nesting_level: 2,
body_blocks: vec![5, 6, 7],
};
assert_eq!(loop_info.header, 5);
assert_eq!(loop_info.back_edge_from, 7);
assert_eq!(loop_info.body_size, 3);
assert_eq!(loop_info.nesting_level, 2);
assert_eq!(loop_info.body_blocks, vec![5, 6, 7]);
}
#[test]
fn test_loops_json_output_format() {
use crate::output::JsonResponse;
let response = LoopsResponse {
function: "json_test".to_string(),
loop_count: 1,
loops: vec![LoopInfo {
header: 1,
back_edge_from: 2,
body_size: 2,
nesting_level: 0,
body_blocks: vec![1, 2],
}],
};
let wrapper = JsonResponse::new(response);
let json = wrapper.to_json();
assert!(json.contains("\"schema_version\""));
assert!(json.contains("\"execution_id\""));
assert!(json.contains("\"tool\""));
assert!(json.contains("\"timestamp\""));
assert!(json.contains("\"data\""));
}
#[test]
fn test_loops_verbose_flag() {
let args_verbose = LoopsArgs {
function: "test".to_string(),
file: None,
verbose: true,
};
let args_not_verbose = LoopsArgs {
function: "test".to_string(),
file: None,
verbose: false,
};
assert!(args_verbose.verbose);
assert!(!args_not_verbose.verbose);
}
#[test]
fn test_loops_nesting_levels() {
let loop_outer = LoopInfo {
header: 1,
back_edge_from: 3,
body_size: 3,
nesting_level: 0, body_blocks: vec![1, 2, 3],
};
let loop_inner = LoopInfo {
header: 2,
back_edge_from: 4,
body_size: 2,
nesting_level: 1, body_blocks: vec![2, 4],
};
assert_eq!(loop_outer.nesting_level, 0);
assert_eq!(loop_inner.nesting_level, 1);
}
#[test]
fn test_loops_response_empty() {
use crate::output::JsonResponse;
let response = LoopsResponse {
function: "no_loops_func".to_string(),
loop_count: 0,
loops: vec![],
};
let wrapper = JsonResponse::new(response);
let json = wrapper.to_json();
assert!(json.contains("\"loop_count\":0"));
assert!(json.contains("\"loops\":[]"));
}
#[test]
fn test_patterns_if_else_detection() {
use crate::cfg::{detect_if_else_patterns, detect_match_patterns};
let cfg = cmds::create_test_cfg();
let if_else_patterns = detect_if_else_patterns(&cfg);
let match_patterns = detect_match_patterns(&cfg);
assert!(
!if_else_patterns.is_empty(),
"Should detect if/else pattern"
);
let pattern = &if_else_patterns[0];
assert_eq!(cfg[pattern.condition].id, 1);
assert_eq!(cfg[pattern.true_branch].id, 2);
assert_eq!(cfg[pattern.false_branch].id, 3);
assert!(
match_patterns.is_empty(),
"Should not detect match patterns in simple if/else"
);
}
#[test]
fn test_patterns_if_else_filter() {
let args = PatternsArgs {
function: "test_func".to_string(),
file: None,
if_else: true,
r#match: false,
};
assert!(args.if_else);
assert!(!args.r#match);
assert_eq!(args.function, "test_func");
}
#[test]
fn test_patterns_match_filter() {
let args = PatternsArgs {
function: "test_func".to_string(),
file: None,
if_else: false,
r#match: true,
};
assert!(!args.if_else);
assert!(args.r#match);
assert_eq!(args.function, "test_func");
}
#[test]
fn test_patterns_json_output() {
let args = PatternsArgs {
function: "test_func".to_string(),
file: None,
if_else: false,
r#match: false,
};
let cli = Cli {
db: None,
output: OutputFormat::Json,
command: Some(Commands::Patterns(args.clone())),
detect_backend: false,
};
assert!(matches!(cli.output, OutputFormat::Json));
}
#[test]
fn test_patterns_response_serialization() {
let response = PatternsResponse {
function: "test_func".to_string(),
if_else_count: 1,
match_count: 0,
if_else_patterns: vec![IfElseInfo {
condition_block: 1,
true_branch: 2,
false_branch: 3,
merge_point: Some(4),
has_else: true,
}],
match_patterns: vec![],
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"function\""));
assert!(json.contains("\"if_else_count\""));
assert!(json.contains("\"match_count\""));
assert!(json.contains("\"if_else_patterns\""));
assert!(json.contains("\"condition_block\""));
assert!(json.contains("\"merge_point\""));
}
}
#[cfg(test)]
mod frontiers_tests {
use super::*;
use crate::cfg::{compute_dominance_frontiers, DominatorTree};
use tempfile::NamedTempFile;
fn create_minimal_db() -> anyhow::Result<NamedTempFile> {
use crate::storage::{
REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
};
let file = NamedTempFile::new()?;
let conn = rusqlite::Connection::open(file.path())?;
conn.execute(
"CREATE TABLE magellan_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
magellan_schema_version INTEGER NOT NULL,
sqlitegraph_schema_version INTEGER NOT NULL,
created_at INTEGER NOT NULL
)",
[],
)?;
conn.execute(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY,
type TEXT NOT NULL,
name TEXT,
source_file TEXT
)",
[],
)?;
conn.execute(
"INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
VALUES (1, ?, ?, strftime('%s', 'now'))",
[REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION],
)?;
Ok(file)
}
#[test]
fn test_frontiers_response_serialization() {
use crate::output::JsonResponse;
let response = FrontiersResponse {
function: "test_func".to_string(),
nodes_with_frontiers: 2,
frontiers: vec![
NodeFrontier {
node: 1,
frontier_set: vec![3],
},
NodeFrontier {
node: 2,
frontier_set: vec![3],
},
],
};
let wrapper = JsonResponse::new(response);
let json = wrapper.to_json();
assert!(json.contains("\"function\":\"test_func\""));
assert!(json.contains("\"nodes_with_frontiers\":2"));
assert!(json.contains("\"frontiers\":["));
}
#[test]
fn test_iterated_frontier_response_serialization() {
use crate::output::JsonResponse;
let response = IteratedFrontierResponse {
function: "test_func".to_string(),
iterated_frontier: vec![3, 4],
};
let wrapper = JsonResponse::new(response);
let json = wrapper.to_json();
assert!(json.contains("\"function\":\"test_func\""));
assert!(json.contains("\"iterated_frontier\":[3,4]"));
}
#[test]
fn test_frontiers_basic() {
use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
use petgraph::graph::DiGraph;
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![1],
otherwise: 2,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec!["branch 1".to_string()],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec!["branch 2".to_string()],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::TrueBranch);
g.add_edge(b0, b2, EdgeType::FalseBranch);
g.add_edge(b1, b3, EdgeType::Fallthrough);
g.add_edge(b2, b3, EdgeType::Fallthrough);
let dom_tree = DominatorTree::new(&g).expect("CFG has entry");
let frontiers = compute_dominance_frontiers(&g, dom_tree);
let df1 = frontiers.frontier(b1);
assert!(df1.contains(&b3));
assert_eq!(df1.len(), 1);
let df2 = frontiers.frontier(b2);
assert!(df2.contains(&b3));
assert_eq!(df2.len(), 1);
let df0 = frontiers.frontier(b0);
assert!(df0.is_empty());
}
#[test]
fn test_frontiers_iterated_flag() {
let args = FrontiersArgs {
function: "test_func".to_string(),
file: None,
iterated: true,
node: None,
};
assert!(args.iterated);
assert!(args.node.is_none());
}
#[test]
fn test_frontiers_node_flag() {
let args = FrontiersArgs {
function: "test_func".to_string(),
file: None,
iterated: false,
node: Some(5),
};
assert!(!args.iterated);
assert_eq!(args.node, Some(5));
}
#[test]
fn test_frontiers_linear_cfg() {
use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
use petgraph::graph::DiGraph;
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 2 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::Goto { target: 3 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::Fallthrough);
g.add_edge(b2, b3, EdgeType::Fallthrough);
let dom_tree = DominatorTree::new(&g).expect("CFG has entry");
let frontiers = compute_dominance_frontiers(&g, dom_tree);
let nodes_with_frontiers: Vec<_> = frontiers.nodes_with_frontiers().collect();
assert!(nodes_with_frontiers.is_empty());
}
#[test]
fn test_frontiers_loop_cfg() {
use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
use petgraph::graph::DiGraph;
let mut g = DiGraph::new();
let b0 = g.add_node(BasicBlock {
id: 0,
db_id: None,
kind: BlockKind::Entry,
statements: vec![],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b1 = g.add_node(BasicBlock {
id: 1,
db_id: None,
kind: BlockKind::Normal,
statements: vec![],
terminator: Terminator::SwitchInt {
targets: vec![2],
otherwise: 3,
},
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b2 = g.add_node(BasicBlock {
id: 2,
db_id: None,
kind: BlockKind::Normal,
statements: vec!["loop body".to_string()],
terminator: Terminator::Goto { target: 1 },
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
let b3 = g.add_node(BasicBlock {
id: 3,
db_id: None,
kind: BlockKind::Exit,
statements: vec![],
terminator: Terminator::Return,
source_location: None,
coord_x: 0,
coord_y: 0,
coord_z: 0,
});
g.add_edge(b0, b1, EdgeType::Fallthrough);
g.add_edge(b1, b2, EdgeType::TrueBranch);
g.add_edge(b1, b3, EdgeType::FalseBranch);
g.add_edge(b2, b1, EdgeType::LoopBack);
let dom_tree = DominatorTree::new(&g).expect("CFG has entry");
let frontiers = compute_dominance_frontiers(&g, dom_tree);
let df1 = frontiers.frontier(b1);
assert!(df1.contains(&b1), "Loop header should have self-frontier");
}
#[test]
fn test_frontiers_json_output_format() {
use crate::output::JsonResponse;
let response = FrontiersResponse {
function: "json_test".to_string(),
nodes_with_frontiers: 2,
frontiers: vec![
NodeFrontier {
node: 1,
frontier_set: vec![3],
},
NodeFrontier {
node: 2,
frontier_set: vec![3],
},
],
};
let wrapper = JsonResponse::new(response);
let json = wrapper.to_json();
assert!(json.contains("\"schema_version\""));
assert!(json.contains("\"execution_id\""));
assert!(json.contains("\"tool\""));
assert!(json.contains("\"timestamp\""));
assert!(json.contains("\"data\""));
}
#[test]
fn test_frontiers_response_empty() {
use crate::output::JsonResponse;
let response = FrontiersResponse {
function: "linear_func".to_string(),
nodes_with_frontiers: 0,
frontiers: vec![],
};
let wrapper = JsonResponse::new(response);
let json = wrapper.to_json();
assert!(json.contains("\"nodes_with_frontiers\":0"));
assert!(json.contains("\"frontiers\":[]"));
}
#[test]
fn test_hotspots_args_parsing() {
let args = HotspotsArgs {
entry: "main".to_string(),
top: 10,
min_paths: Some(5),
verbose: true,
inter_procedural: false,
intra_procedural: false,
};
assert_eq!(args.entry, "main");
assert_eq!(args.top, 10);
assert_eq!(args.min_paths, Some(5));
assert!(args.verbose);
assert!(!args.inter_procedural);
}
#[test]
fn test_hotspots_args_default_entry() {
let args = HotspotsArgs {
entry: "main".to_string(), top: 20,
min_paths: None,
verbose: false,
inter_procedural: false,
intra_procedural: false,
};
assert_eq!(args.entry, "main");
assert_eq!(args.top, 20); }
#[test]
fn test_hotspot_entry_serialization() {
let entry = HotspotEntry {
function: "test_func".to_string(),
risk_score: 42.5,
path_count: 10,
dominance_factor: 1.5,
complexity: 5,
file_path: "test.rs".to_string(),
};
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("test_func"));
assert!(json.contains("42.5"));
assert!(json.contains("\"path_count\":10"));
}
#[test]
fn test_hotspots_response_serialization() {
use crate::output::JsonResponse;
let response = HotspotsResponse {
entry_point: "main".to_string(),
total_functions: 100,
hotspots: vec![],
mode: "intra-procedural".to_string(),
};
let wrapper = JsonResponse::new(response);
let json = wrapper.to_json();
assert!(json.contains("\"entry_point\":\"main\""));
assert!(json.contains("\"total_functions\":100"));
assert!(json.contains("intra-procedural"));
}
#[test]
fn test_hotspots_response_with_entries() {
use crate::output::JsonResponse;
let hotspot = HotspotEntry {
function: "risky_func".to_string(),
risk_score: 85.0,
path_count: 50,
dominance_factor: 3.0,
complexity: 15,
file_path: "src/lib.rs".to_string(),
};
let response = HotspotsResponse {
entry_point: "main".to_string(),
total_functions: 10,
hotspots: vec![hotspot],
mode: "inter-procedural".to_string(),
};
let wrapper = JsonResponse::new(response);
let json = wrapper.to_json();
assert!(json.contains("risky_func"));
assert!(json.contains("85"));
assert!(json.contains("inter-procedural"));
}
#[test]
fn test_hotspot_entry_clone() {
let entry = HotspotEntry {
function: "func".to_string(),
risk_score: 1.0,
path_count: 1,
dominance_factor: 1.0,
complexity: 1,
file_path: "file.rs".to_string(),
};
let cloned = entry.clone();
assert_eq!(entry.function, cloned.function);
assert_eq!(entry.risk_score, cloned.risk_score);
}
#[test]
fn test_hotpaths_args_parsing() {
let args = HotpathsArgs {
function: "my_function".to_string(),
top: 5,
rationale: true,
min_score: Some(0.5),
};
assert_eq!(args.function, "my_function");
assert_eq!(args.top, 5);
assert!(args.rationale);
assert_eq!(args.min_score, Some(0.5));
}
#[test]
fn test_hotpaths_args_defaults() {
let args = HotpathsArgs {
function: "main".to_string(),
top: 10, rationale: false,
min_score: None,
};
assert_eq!(args.function, "main");
assert_eq!(args.top, 10); assert!(!args.rationale);
assert!(args.min_score.is_none());
}
#[test]
fn test_dominators_args_has_inter_procedural_flag() {
let args = DominatorsArgs {
function: "main".to_string(),
file: None,
must_pass_through: Some("block1".to_string()),
post: false,
inter_procedural: true,
};
assert!(args.inter_procedural);
assert_eq!(args.function, "main");
assert_eq!(args.must_pass_through, Some("block1".to_string()));
assert!(!args.post);
}
#[test]
fn test_dominators_args_default_intra_procedural() {
let args = DominatorsArgs {
function: "main".to_string(),
file: None,
must_pass_through: None,
post: false,
inter_procedural: false, };
assert!(!args.inter_procedural);
assert!(!args.post);
assert!(args.must_pass_through.is_none());
}
#[test]
fn test_dominators_inter_procedural_with_post() {
let args = DominatorsArgs {
function: "entry".to_string(),
file: None,
must_pass_through: None,
post: true,
inter_procedural: true,
};
assert!(args.inter_procedural);
assert!(args.post);
}
#[test]
fn test_dominators_inter_procedural_must_pass_through_combination() {
let args = DominatorsArgs {
function: "main".to_string(),
file: None,
must_pass_through: Some("some_block".to_string()),
post: false,
inter_procedural: true,
};
assert!(args.inter_procedural);
assert!(args.must_pass_through.is_some());
}
}