1use clap::{Parser, Subcommand, ValueEnum};
4
5pub use crate::analysis::DeadSymbolJson;
7
8#[derive(Parser, Debug, Clone)]
13#[command(name = "mirage")]
14#[command(author, version, about)]
15#[command(
16 long_about = "Mirage is a path-aware code intelligence engine that operates on graphs, not text.
17
18It materializes behavior explicitly: paths, proofs, counterexamples.
19
20NOT:
21 - A search tool (llmgrep already does this)
22 - An embedding tool
23 - Static analysis / linting
24
25IS:
26 - Path enumeration and verification
27 - Graph-based reasoning about code behavior
28 - Truth engine that materializes facts for LLM consumption
29
30The Golden Rule: An agent may only speak if it can reference a graph artifact."
31)]
32pub struct Cli {
33 #[arg(global = true, long, env = "MIRAGE_DB")]
35 pub db: Option<String>,
36
37 #[arg(global = true, long, value_enum, default_value_t = OutputFormat::Human)]
39 pub output: OutputFormat,
40
41 #[arg(long, global = true, default_value = "false")]
43 pub detect_backend: bool,
44
45 #[command(subcommand)]
46 pub command: Option<Commands>,
47}
48
49#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
51pub enum OutputFormat {
52 Human,
54 Json,
56 Pretty,
58}
59
60#[derive(Subcommand, Debug, Clone)]
61pub enum Commands {
62 Status(StatusArgs),
64
65 Paths(PathsArgs),
67
68 Cfg(CfgArgs),
70
71 Dominators(DominatorsArgs),
73
74 Loops(LoopsArgs),
76
77 Unreachable(UnreachableArgs),
79
80 Patterns(PatternsArgs),
82
83 Frontiers(FrontiersArgs),
85
86 Verify(VerifyArgs),
88
89 BlastZone(BlastZoneArgs),
91
92 Cycles(CyclesArgs),
94
95 Slice(SliceArgs),
97
98 Hotspots(HotspotsArgs),
100
101 Hotpaths(HotpathsArgs),
103
104 Diff(DiffArgs),
106
107 Icfg(IcfgArgs),
109
110 Coverage(CoverageArgs),
112
113 Migrate(MigrateArgs),
115}
116
117#[derive(Parser, Debug, Clone, Copy)]
122pub struct StatusArgs {}
123
124#[derive(Parser, Debug, Clone)]
125pub struct PathsArgs {
126 #[arg(long)]
128 pub function: String,
129
130 #[arg(long)]
132 pub file: Option<String>,
133
134 #[arg(long)]
136 pub show_errors: bool,
137
138 #[arg(long)]
140 pub max_length: Option<usize>,
141
142 #[arg(long)]
144 pub with_blocks: bool,
145
146 #[arg(long)]
148 pub incremental: bool,
149
150 #[arg(long)]
152 pub since: Option<String>,
153
154 #[arg(long)]
156 pub by_coverage: bool,
157}
158
159#[derive(Parser, Debug, Clone)]
160pub struct CfgArgs {
161 #[arg(long)]
163 pub function: String,
164
165 #[arg(long)]
167 pub file: Option<String>,
168
169 #[arg(long, value_enum)]
171 pub format: Option<CfgFormat>,
172}
173
174#[derive(Parser, Debug, Clone)]
175pub struct CoverageArgs {
176 #[arg(long)]
178 pub function: String,
179
180 #[arg(long)]
182 pub file: Option<String>,
183}
184
185#[derive(Parser, Debug, Clone)]
186pub struct DominatorsArgs {
187 #[arg(long)]
189 pub function: String,
190
191 #[arg(long)]
193 pub file: Option<String>,
194
195 #[arg(long)]
197 pub must_pass_through: Option<String>,
198
199 #[arg(long)]
201 pub post: bool,
202
203 #[arg(long)]
205 pub inter_procedural: bool,
206}
207
208#[derive(Parser, Debug, Clone)]
209pub struct LoopsArgs {
210 #[arg(long)]
212 pub function: String,
213
214 #[arg(long)]
216 pub file: Option<String>,
217
218 #[arg(long)]
220 pub verbose: bool,
221}
222
223#[derive(Parser, Debug, Clone)]
224pub struct UnreachableArgs {
225 #[arg(long)]
227 pub within_functions: bool,
228
229 #[arg(long)]
231 pub show_branches: bool,
232
233 #[arg(long)]
235 pub include_uncalled: bool,
236}
237
238#[derive(Parser, Debug, Clone)]
239pub struct PatternsArgs {
240 #[arg(long)]
242 pub function: String,
243
244 #[arg(long)]
246 pub file: Option<String>,
247
248 #[arg(long)]
250 pub if_else: bool,
251
252 #[arg(long)]
254 pub r#match: bool,
255}
256
257#[derive(Parser, Debug, Clone)]
258pub struct FrontiersArgs {
259 #[arg(long)]
261 pub function: String,
262
263 #[arg(long)]
265 pub file: Option<String>,
266
267 #[arg(long)]
269 pub iterated: bool,
270
271 #[arg(long)]
273 pub node: Option<usize>,
274}
275
276#[derive(Parser, Debug, Clone)]
277pub struct VerifyArgs {
278 #[arg(long)]
280 pub path_id: String,
281}
282
283#[derive(Parser, Debug, Clone)]
284pub struct BlastZoneArgs {
285 #[arg(long)]
287 pub function: Option<String>,
288
289 #[arg(long)]
291 pub file: Option<String>,
292
293 #[arg(long)]
295 pub block_id: Option<usize>,
296
297 #[arg(long)]
299 pub path_id: Option<String>,
300
301 #[arg(long, default_value_t = 100)]
303 pub max_depth: usize,
304
305 #[arg(long)]
307 pub include_errors: bool,
308
309 #[arg(long)]
311 pub use_call_graph: bool,
312}
313
314#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
316pub enum CycleTypeArg {
317 All,
319 InterFunction,
321 SelfLoop,
323}
324
325#[derive(Parser, Debug, Clone)]
326pub struct CyclesArgs {
327 #[arg(long)]
329 pub call_graph: bool,
330
331 #[arg(long)]
333 pub function_loops: bool,
334
335 #[arg(long)]
337 pub both: bool,
338
339 #[arg(long, value_enum, default_value = "all")]
341 pub cycle_type: CycleTypeArg,
342
343 #[arg(long)]
345 pub verbose: bool,
346}
347
348#[derive(Parser, Debug, Clone)]
349pub struct SliceArgs {
350 #[arg(long)]
352 pub symbol: String,
353
354 #[arg(long, value_enum)]
356 pub direction: SliceDirectionArg,
357
358 #[arg(long)]
360 pub verbose: bool,
361}
362
363#[derive(Parser, Debug, Clone)]
364pub struct HotspotsArgs {
365 #[arg(long, default_value = "main")]
367 pub entry: String,
368
369 #[arg(long, default_value = "20")]
371 pub top: usize,
372
373 #[arg(long)]
375 pub min_paths: Option<usize>,
376
377 #[arg(long)]
379 pub verbose: bool,
380
381 #[arg(long, default_value = "true")]
384 pub inter_procedural: bool,
385
386 #[arg(long, conflicts_with = "inter_procedural")]
388 pub intra_procedural: bool,
389}
390
391#[derive(Parser, Debug, Clone)]
393pub struct HotpathsArgs {
394 #[arg(long)]
396 pub function: String,
397
398 #[arg(long, default_value = "10")]
400 pub top: usize,
401
402 #[arg(long)]
404 pub rationale: bool,
405
406 #[arg(long)]
408 pub min_score: Option<f64>,
409}
410
411#[derive(Parser, Debug, Clone)]
413pub struct MigrateArgs {
414 #[arg(long, value_enum)]
416 pub from: BackendFormat,
417
418 #[arg(long, value_enum)]
420 pub to: BackendFormat,
421
422 #[arg(short, long)]
424 pub db: String,
425
426 #[arg(long)]
428 pub backup: bool,
429
430 #[arg(long)]
432 pub dry_run: bool,
433}
434
435#[derive(Parser, Debug, Clone)]
437pub struct IcfgArgs {
438 #[arg(long)]
440 pub entry: String,
441
442 #[arg(long, default_value = "3")]
444 pub depth: usize,
445
446 #[arg(long, default_value = "true")]
448 pub return_edges: bool,
449
450 #[arg(long, value_enum)]
452 pub format: Option<IcfgFormat>,
453}
454
455#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
457pub enum IcfgFormat {
458 Dot,
460 Json,
462 Human,
464}
465
466#[derive(Parser, Debug, Clone)]
468pub struct DiffArgs {
469 #[arg(long)]
471 pub function: String,
472
473 #[arg(long)]
475 pub before: String,
476
477 #[arg(long)]
479 pub after: String,
480
481 #[arg(long)]
483 pub show_edges: bool,
484
485 #[arg(long)]
487 pub verbose: bool,
488}
489
490#[derive(clap::ValueEnum, Clone, Debug, Copy, PartialEq, Eq)]
492pub enum BackendFormat {
493 Sqlite,
495 Geometric,
497}
498
499impl std::fmt::Display for BackendFormat {
500 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
501 match self {
502 Self::Sqlite => write!(f, "sqlite"),
503 Self::Geometric => write!(f, "geometric"),
504 }
505 }
506}
507
508#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
509pub enum SliceDirectionArg {
510 Backward,
512 Forward,
514}
515
516#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
518pub enum CfgFormat {
519 Human,
521 Dot,
523 Json,
525}
526
527pub fn resolve_db_path(cli_db: Option<String>) -> anyhow::Result<String> {
536 if let Some(path) = cli_db {
537 return Ok(path);
538 }
539
540 if let Ok(path) = std::env::var("MIRAGE_DB") {
542 return Ok(path);
543 }
544
545 if let Some(path) = auto_discover_db() {
547 eprintln!("Info: Auto-discovered database at {}", path);
548 return Ok(path);
549 }
550
551 Err(anyhow::anyhow!(
552 "No database specified. Use --db, set MIRAGE_DB env var, \
553 or run from a directory with a .db file"
554 ))
555}
556
557fn auto_discover_db() -> Option<String> {
565 use std::path::Path;
566
567 let search_dirs = [".magellan", ".forge", "."];
569
570 for dir in &search_dirs {
571 if let Ok(entries) = std::fs::read_dir(dir) {
572 let mut db_files: Vec<_> = entries
573 .filter_map(|e| e.ok())
574 .filter(|e| {
575 let path = e.path();
576 path.extension().map(|ext| ext == "db").unwrap_or(false)
577 })
578 .map(|e| e.path())
579 .collect();
580
581 db_files.sort();
583
584 if let Some(preferred) = db_files.iter().find(|p| {
586 let name = p
587 .file_stem()
588 .map(|s| s.to_string_lossy())
589 .unwrap_or_default();
590 name == "magellan" || name == "mirage"
591 }) {
592 return Some(preferred.to_string_lossy().to_string());
593 }
594
595 if let Some(first) = db_files.first() {
597 return Some(first.to_string_lossy().to_string());
598 }
599 }
600 }
601
602 let candidates = [
604 ".magellan/mirage.db",
605 ".magellan/magellan.db",
606 "mirage.db",
607 "magellan.db",
608 "graph.db",
609 ];
610 for name in &candidates {
611 if Path::new(name).exists() {
612 return Some(name.to_string());
613 }
614 }
615
616 None
617}
618
619fn detect_repo_path(db_path: &str) -> std::path::PathBuf {
624 use std::path::Path;
625
626 let db_path = Path::new(db_path);
627
628 let mut path = if db_path.is_absolute() {
630 db_path.to_path_buf()
631 } else {
632 std::env::current_dir()
633 .map(|cwd| cwd.join(db_path))
634 .unwrap_or_else(|_| db_path.to_path_buf())
635 };
636
637 while path.pop() {
639 let git_dir = path.join(".git");
640 if git_dir.exists() {
641 return path;
642 }
643 }
644
645 Path::new(".").to_path_buf()
647}
648
649#[derive(serde::Serialize)]
655struct PathsResponse {
656 function: String,
657 total_paths: usize,
658 error_paths: usize,
659 paths: Vec<PathSummary>,
660}
661
662#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
664struct PathBlock {
665 block_id: usize,
666 terminator: String,
667}
668
669#[derive(serde::Serialize)]
671struct SourceRange {
672 file_path: String,
673 start_line: usize,
674 end_line: usize,
675}
676
677#[derive(serde::Serialize)]
679struct PathSummary {
680 path_id: String,
681 kind: String,
682 length: usize,
683 blocks: Vec<PathBlock>,
684 summary: Option<String>,
686 source_range: Option<SourceRange>,
688}
689
690impl From<crate::cfg::Path> for PathSummary {
691 fn from(path: crate::cfg::Path) -> Self {
692 let length = path.len();
693 let blocks: Vec<PathBlock> = path
696 .blocks
697 .into_iter()
698 .map(|block_id| PathBlock {
699 block_id,
700 terminator: "Unknown".to_string(),
701 })
702 .collect();
703
704 Self {
705 path_id: path.path_id,
706 kind: format!("{:?}", path.kind),
707 length,
708 blocks,
709 summary: None, source_range: None, }
712 }
713}
714
715impl PathSummary {
716 pub fn from_with_cfg(path: crate::cfg::Path, cfg: &crate::cfg::Cfg) -> Self {
719 use crate::cfg::summarize_path;
720
721 let summary = Some(summarize_path(cfg, &path));
723
724 let blocks: Vec<PathBlock> = path
726 .blocks
727 .iter()
728 .map(|&block_id| {
729 let node_idx = cfg.node_indices().find(|&n| cfg[n].id == block_id);
731
732 let terminator = match node_idx {
733 Some(idx) => format!("{:?}", cfg[idx].terminator),
734 None => "Unknown".to_string(),
735 };
736
737 PathBlock {
738 block_id,
739 terminator,
740 }
741 })
742 .collect();
743
744 let source_range = Self::calculate_source_range(&path, cfg);
746
747 let length = path.len();
748
749 Self {
750 path_id: path.path_id,
751 kind: format!("{:?}", path.kind),
752 length,
753 summary,
754 source_range,
755 blocks,
756 }
757 }
758
759 fn calculate_source_range(
761 path: &crate::cfg::Path,
762 cfg: &crate::cfg::Cfg,
763 ) -> Option<SourceRange> {
764 let first_loc = path
765 .blocks
766 .first()
767 .and_then(|&bid| cfg.node_indices().find(|&n| cfg[n].id == bid))
768 .and_then(|idx| cfg[idx].source_location.clone());
769
770 let last_loc = path
771 .blocks
772 .last()
773 .and_then(|&bid| cfg.node_indices().find(|&n| cfg[n].id == bid))
774 .and_then(|idx| cfg[idx].source_location.clone());
775
776 match (first_loc, last_loc) {
777 (Some(first), Some(last)) => {
778 Some(SourceRange {
780 file_path: first.file_path.to_string_lossy().to_string(),
781 start_line: first.start_line,
782 end_line: last.end_line,
783 })
784 }
785 _ => None,
786 }
787 }
788}
789
790#[derive(serde::Serialize)]
792struct DominanceResponse {
793 function: String,
794 kind: String, root: Option<usize>,
796 dominance_tree: Vec<DominatorEntry>,
797 must_pass_through: Option<MustPassThroughResult>,
798}
799
800#[derive(serde::Serialize)]
802struct DominatorEntry {
803 block: usize,
804 immediate_dominator: Option<usize>,
805 dominated: Vec<usize>,
806}
807
808#[derive(serde::Serialize)]
810struct MustPassThroughResult {
811 block: usize,
812 must_pass: Vec<usize>,
813}
814
815#[derive(serde::Serialize)]
817struct InterProceduralDominanceResponse {
818 function: String,
820 kind: String,
822 dominator_count: usize,
824 dominators: Vec<String>,
826}
827
828#[derive(serde::Serialize)]
830struct UnreachableResponse {
831 function: String,
832 total_functions: usize,
833 functions_with_unreachable: usize,
834 unreachable_count: usize,
835 blocks: Vec<UnreachableBlock>,
836 #[serde(skip_serializing_if = "Option::is_none")]
838 uncalled_functions: Option<Vec<DeadSymbolJson>>,
839}
840
841#[derive(serde::Serialize, Clone)]
843struct IncomingEdge {
844 from_block: usize,
845 edge_type: String,
846}
847
848#[derive(serde::Serialize, Clone)]
850struct UnreachableBlock {
851 block_id: usize,
852 kind: String,
853 statements: Vec<String>,
854 terminator: String,
855 #[serde(skip_serializing_if = "Vec::is_empty")]
856 incoming_edges: Vec<IncomingEdge>,
857}
858
859#[derive(serde::Serialize)]
861struct VerifyResult {
862 path_id: String,
863 valid: bool,
864 found_in_cache: bool,
865 function_id: Option<i64>,
866 reason: String,
867 current_paths: usize,
868}
869
870#[derive(serde::Serialize)]
872struct LoopsResponse {
873 function: String,
874 loop_count: usize,
875 loops: Vec<LoopInfo>,
876}
877
878#[derive(serde::Serialize)]
880struct LoopInfo {
881 header: usize,
882 back_edge_from: usize,
883 body_size: usize,
884 nesting_level: usize,
885 body_blocks: Vec<usize>,
886}
887
888#[derive(serde::Serialize)]
890struct PatternsResponse {
891 function: String,
892 if_else_count: usize,
893 match_count: usize,
894 if_else_patterns: Vec<IfElseInfo>,
895 match_patterns: Vec<MatchInfo>,
896}
897
898#[derive(serde::Serialize)]
900struct IfElseInfo {
901 condition_block: usize,
902 true_branch: usize,
903 false_branch: usize,
904 merge_point: Option<usize>,
905 has_else: bool,
906}
907
908#[derive(serde::Serialize)]
910struct MatchInfo {
911 switch_block: usize,
912 branch_count: usize,
913 targets: Vec<usize>,
914 otherwise: usize,
915}
916
917#[derive(serde::Serialize)]
919struct FrontiersResponse {
920 function: String,
921 nodes_with_frontiers: usize,
922 frontiers: Vec<NodeFrontier>,
923}
924
925#[derive(serde::Serialize)]
927struct NodeFrontier {
928 node: usize,
929 frontier_set: Vec<usize>,
930}
931
932#[derive(serde::Serialize)]
934struct IteratedFrontierResponse {
935 function: String,
936 iterated_frontier: Vec<usize>,
937}
938
939#[derive(serde::Serialize)]
941struct BlockImpactResponse {
942 function: String,
943 block_id: usize,
944 reachable_blocks: Vec<usize>,
945 reachable_count: usize,
946 max_depth: usize,
947 has_cycles: bool,
948 #[serde(skip_serializing_if = "Option::is_none")]
949 forward_impact: Option<Vec<CallGraphSymbol>>,
950 #[serde(skip_serializing_if = "Option::is_none")]
951 backward_impact: Option<Vec<CallGraphSymbol>>,
952}
953
954#[derive(serde::Serialize)]
956struct PathImpactResponse {
957 path_id: String,
958 path_length: usize,
959 unique_blocks_affected: Vec<usize>,
960 impact_count: usize,
961 #[serde(skip_serializing_if = "Option::is_none")]
962 forward_impact: Option<Vec<CallGraphSymbol>>,
963 #[serde(skip_serializing_if = "Option::is_none")]
964 backward_impact: Option<Vec<CallGraphSymbol>>,
965}
966
967#[derive(Clone, serde::Serialize)]
969struct CallGraphSymbol {
970 #[serde(skip_serializing_if = "Option::is_none")]
971 symbol_id: Option<String>,
972 #[serde(skip_serializing_if = "Option::is_none")]
973 fqn: Option<String>,
974 file_path: String,
975 kind: String,
976}
977
978#[derive(serde::Serialize)]
980struct HotspotsResponse {
981 entry_point: String,
983 total_functions: usize,
985 hotspots: Vec<HotspotEntry>,
987 mode: String, }
990
991#[derive(serde::Serialize, Clone)]
993struct HotspotEntry {
994 function: String,
996 risk_score: f64,
998 path_count: usize,
1000 dominance_factor: f64,
1002 complexity: usize,
1004 file_path: String,
1006}
1007
1008pub mod cmds {
1013 use super::*;
1014 use crate::output;
1015 use anyhow::Result;
1016
1017 pub fn status(_args: &StatusArgs, cli: &Cli) -> Result<()> {
1018 use crate::storage::MirageDb;
1019
1020 let db_path = super::resolve_db_path(cli.db.clone())?;
1022
1023 let db = match MirageDb::open(&db_path) {
1025 Ok(db) => db,
1026 Err(e) => {
1027 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1029 let error = output::JsonError::database_not_found(&db_path);
1030 let wrapper = output::JsonResponse::new(error);
1031 println!("{}", wrapper.to_json());
1032 std::process::exit(output::EXIT_DATABASE);
1033 } else {
1034 output::error(&format!("Failed to open database: {}", db_path));
1035 output::error(&format!("Error details: {}", e));
1036 output::info("Hint: Run 'magellan watch' to create the database");
1037 std::process::exit(output::EXIT_DATABASE);
1038 }
1039 }
1040 };
1041
1042 let status = db.status()?;
1044
1045 match cli.output {
1049 OutputFormat::Human => {
1050 println!("Mirage Database Status:");
1052 println!(
1053 " Schema version: {} (Magellan: {})",
1054 status.mirage_schema_version, status.magellan_schema_version
1055 );
1056 println!(" cfg_blocks: {}", status.cfg_blocks);
1057 println!(
1060 " cfg_paths: {} (use 'mirage paths --function <name>' to enumerate)",
1061 status.cfg_paths
1062 );
1063 println!(" cfg_dominators: {}", status.cfg_dominators);
1064 }
1065 OutputFormat::Json => {
1066 let response = output::JsonResponse::new(status);
1068 println!("{}", response.to_json());
1069 }
1070 OutputFormat::Pretty => {
1071 let response = output::JsonResponse::new(status);
1073 println!("{}", response.to_pretty_json());
1074 }
1075 }
1076
1077 Ok(())
1078 }
1079
1080 pub fn paths(args: &PathsArgs, cli: &Cli) -> Result<()> {
1081 use crate::cfg::{
1082 enumerate_paths_incremental, get_or_enumerate_paths, PathKind, PathLimits,
1083 };
1084 use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
1085 use crate::storage::{get_function_hash_db, MirageDb};
1086
1087 let db_path = super::resolve_db_path(cli.db.clone())?;
1089
1090 let repo_path = detect_repo_path(&db_path);
1092
1093 if args.incremental {
1095 let since = args
1096 .since
1097 .as_ref()
1098 .ok_or_else(|| anyhow::anyhow!("--since required with --incremental"))?;
1099
1100 let db = match MirageDb::open(&db_path) {
1102 Ok(db) => db,
1103 Err(_e) => {
1104 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1105 let error = output::JsonError::database_not_found(&db_path);
1106 let wrapper = output::JsonResponse::new(error);
1107 println!("{}", wrapper.to_json());
1108 std::process::exit(output::EXIT_DATABASE);
1109 } else {
1110 output::error(&format!("Failed to open database: {}", db_path));
1111 output::info("Hint: Run 'magellan watch' to create the database");
1112 std::process::exit(output::EXIT_DATABASE);
1113 }
1114 }
1115 };
1116
1117 let result = match enumerate_paths_incremental(
1119 &args.function,
1120 &db,
1121 &repo_path,
1122 since,
1123 args.max_length,
1124 ) {
1125 Ok(r) => r,
1126 Err(e) => {
1127 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1128 let error = output::JsonError::new(
1129 "IncrementalAnalysisError",
1130 &format!("Incremental analysis failed: {}", e),
1131 output::E_CFG_ERROR,
1132 );
1133 let wrapper = output::JsonResponse::new(error);
1134 println!("{}", wrapper.to_json());
1135 std::process::exit(output::EXIT_DATABASE);
1136 } else {
1137 output::error(&format!("Incremental analysis failed: {}", e));
1138 std::process::exit(output::EXIT_DATABASE);
1139 }
1140 }
1141 };
1142
1143 match cli.output {
1145 OutputFormat::Human => {
1146 println!("Incremental path enumeration (since {}):", since);
1147 println!(" Analyzed functions: {}", result.analyzed_functions);
1148 println!(" Total paths: {}", result.paths.len());
1149
1150 if args.show_errors {
1151 let error_count = result
1152 .paths
1153 .iter()
1154 .filter(|p| matches!(p.kind, PathKind::Error))
1155 .count();
1156 println!(" Error paths: {}", error_count);
1157 }
1158
1159 if !result.paths.is_empty() {
1160 println!("\nPaths:");
1161 for path in &result.paths {
1162 if args.show_errors || !matches!(path.kind, PathKind::Error) {
1163 println!(" {}", path);
1164 }
1165 }
1166 }
1167 }
1168 OutputFormat::Json => {
1169 let response = serde_json::json!({
1170 "incremental": true,
1171 "since": since,
1172 "analyzed_functions": result.analyzed_functions,
1173 "skipped_functions": result.skipped_functions,
1174 "total_paths": result.paths.len(),
1175 "paths": result.paths,
1176 });
1177 println!("{}", serde_json::to_string(&response)?);
1178 }
1179 OutputFormat::Pretty => {
1180 let response = serde_json::json!({
1181 "incremental": true,
1182 "since": since,
1183 "analyzed_functions": result.analyzed_functions,
1184 "skipped_functions": result.skipped_functions,
1185 "total_paths": result.paths.len(),
1186 "paths": result.paths,
1187 });
1188 println!("{}", serde_json::to_string_pretty(&response)?);
1189 }
1190 }
1191
1192 return Ok(());
1193 }
1194
1195 let mut db = match MirageDb::open(&db_path) {
1198 Ok(db) => db,
1199 Err(_e) => {
1200 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1202 let error = output::JsonError::database_not_found(&db_path);
1203 let wrapper = output::JsonResponse::new(error);
1204 println!("{}", wrapper.to_json());
1205 std::process::exit(output::EXIT_DATABASE);
1206 } else {
1207 output::error(&format!("Failed to open database: {}", db_path));
1208 output::info("Hint: Run 'magellan watch' to create the database");
1209 std::process::exit(output::EXIT_DATABASE);
1210 }
1211 }
1212 };
1213
1214 let function_id =
1216 match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
1217 Ok(id) => id,
1218 Err(_e) => {
1219 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1220 let error = output::JsonError::function_not_found(&args.function);
1221 let wrapper = output::JsonResponse::new(error);
1222 println!("{}", wrapper.to_json());
1223 std::process::exit(output::EXIT_DATABASE);
1224 } else {
1225 output::error(&format!(
1226 "Function '{}' not found in database",
1227 args.function
1228 ));
1229 output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
1230 std::process::exit(output::EXIT_DATABASE);
1231 }
1232 }
1233 };
1234
1235 let cfg = match load_cfg_from_db(&db, function_id) {
1237 Ok(cfg) => cfg,
1238 Err(_e) => {
1239 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1240 let error = output::JsonError::new(
1241 "CgfLoadError",
1242 &format!("Failed to load CFG for function '{}'", args.function),
1243 output::E_CFG_ERROR,
1244 );
1245 let wrapper = output::JsonResponse::new(error);
1246 println!("{}", wrapper.to_json());
1247 std::process::exit(output::EXIT_DATABASE);
1248 } else {
1249 output::error(&format!(
1250 "Failed to load CFG for function '{}'",
1251 args.function
1252 ));
1253 output::info("The function may be corrupted. Try re-running 'magellan watch'");
1254 std::process::exit(output::EXIT_DATABASE);
1255 }
1256 }
1257 };
1258
1259 let mut limits = PathLimits::default();
1261 if let Some(max_length) = args.max_length {
1262 limits = limits.with_max_length(max_length);
1263 }
1264
1265 let mut paths = if db.is_sqlite() {
1268 let function_hash = match get_function_hash_db(&db, function_id) {
1270 Some(hash) => hash,
1271 None => {
1272 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1273 let error = output::JsonError::new(
1274 "HashNotFound",
1275 &format!("Function hash not found for '{}'", args.function),
1276 output::E_CFG_ERROR,
1277 );
1278 let wrapper = output::JsonResponse::new(error);
1279 println!("{}", wrapper.to_json());
1280 std::process::exit(output::EXIT_DATABASE);
1281 } else {
1282 output::error(&format!("Function hash not found for '{}'", args.function));
1283 output::info(
1284 "The function data may be incomplete. Try re-running 'magellan watch'",
1285 );
1286 std::process::exit(output::EXIT_DATABASE);
1287 }
1288 }
1289 };
1290
1291 get_or_enumerate_paths(&cfg, function_id, &function_hash, &limits, db.conn_mut()?)
1292 .map_err(|e| anyhow::anyhow!("Path enumeration failed: {}", e))?
1293 } else {
1294 crate::cfg::enumerate_paths(&cfg, &limits)
1297 };
1298
1299 if args.show_errors {
1301 paths.retain(|p| p.kind == PathKind::Error);
1302 }
1303
1304 if args.by_coverage {
1306 let coverage_map: std::collections::HashMap<i64, i64> = db
1307 .conn()
1308 .ok()
1309 .and_then(|conn| {
1310 let sql = "SELECT block_id, hit_count FROM cfg_block_coverage \
1311 WHERE block_id IN (SELECT id FROM cfg_blocks WHERE function_id = ?1)";
1312 let mut stmt = conn.prepare(sql).ok()?;
1313 let rows = stmt.query_map([function_id], |row| {
1314 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
1315 });
1316 let mut map = std::collections::HashMap::new();
1317 if let Ok(iter) = rows {
1318 for item in iter {
1319 if let Ok((block_id, hit_count)) = item {
1320 map.insert(block_id, hit_count);
1321 }
1322 }
1323 }
1324 if map.is_empty() {
1325 None
1326 } else {
1327 Some(map)
1328 }
1329 })
1330 .unwrap_or_default();
1331
1332 let node_hits: std::collections::HashMap<usize, i64> = cfg
1334 .node_indices()
1335 .filter_map(|idx| {
1336 cfg.node_weight(idx).and_then(|b| {
1337 b.db_id
1338 .and_then(|db_id| coverage_map.get(&db_id).copied())
1339 .map(|hits| (b.id, hits))
1340 })
1341 })
1342 .collect();
1343
1344 paths.sort_by(|a, b| {
1345 let total_a: i64 = a
1346 .blocks
1347 .iter()
1348 .map(|bid| node_hits.get(bid).copied().unwrap_or(0))
1349 .sum();
1350 let total_b: i64 = b
1351 .blocks
1352 .iter()
1353 .map(|bid| node_hits.get(bid).copied().unwrap_or(0))
1354 .sum();
1355 total_b.cmp(&total_a) });
1357 }
1358
1359 let error_count = paths.iter().filter(|p| p.kind == PathKind::Error).count();
1361
1362 match cli.output {
1364 OutputFormat::Human => {
1365 println!("Function: {}", args.function);
1367 println!("Total paths: {}", paths.len());
1368 if args.show_errors {
1369 println!("(Showing error paths only)");
1370 } else {
1371 println!("Error paths: {}", error_count);
1372 }
1373 println!();
1374
1375 if paths.is_empty() {
1376 output::info("No paths found");
1377 return Ok(());
1378 }
1379
1380 for (i, path) in paths.iter().enumerate() {
1381 println!("Path {}: {}", i + 1, path.path_id);
1382 println!(" Kind: {:?}", path.kind);
1383 println!(" Length: {} blocks", path.len());
1384 if args.with_blocks {
1385 println!(
1386 " Blocks: {}",
1387 path.blocks
1388 .iter()
1389 .map(|id| id.to_string())
1390 .collect::<Vec<_>>()
1391 .join(" -> ")
1392 );
1393 }
1394 println!();
1395 }
1396 }
1397 OutputFormat::Json => {
1398 let response = PathsResponse {
1400 function: args.function.clone(),
1401 total_paths: paths.len(),
1402 error_paths: error_count,
1403 paths: paths
1404 .iter()
1405 .map(|p| PathSummary::from_with_cfg(p.clone(), &cfg))
1406 .collect(),
1407 };
1408 let wrapper = output::JsonResponse::new(response);
1409 println!("{}", wrapper.to_json());
1410 }
1411 OutputFormat::Pretty => {
1412 let response = PathsResponse {
1414 function: args.function.clone(),
1415 total_paths: paths.len(),
1416 error_paths: error_count,
1417 paths: paths
1418 .iter()
1419 .map(|p| PathSummary::from_with_cfg(p.clone(), &cfg))
1420 .collect(),
1421 };
1422 let wrapper = output::JsonResponse::new(response);
1423 println!("{}", wrapper.to_pretty_json());
1424 }
1425 }
1426
1427 Ok(())
1428 }
1429
1430 pub fn cfg(args: &CfgArgs, cli: &Cli) -> Result<()> {
1431 use crate::cfg::{export_dot, export_json, CFGExport};
1432 use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
1433 use crate::storage::MirageDb;
1434
1435 let db_path = super::resolve_db_path(cli.db.clone())?;
1437
1438 let db = match MirageDb::open(&db_path) {
1440 Ok(db) => db,
1441 Err(_e) => {
1442 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1444 let error = output::JsonError::database_not_found(&db_path);
1445 let wrapper = output::JsonResponse::new(error);
1446 println!("{}", wrapper.to_json());
1447 std::process::exit(output::EXIT_DATABASE);
1448 } else {
1449 output::error(&format!("Failed to open database: {}", db_path));
1450 output::info("Hint: Run 'magellan watch' to create the database");
1451 std::process::exit(output::EXIT_DATABASE);
1452 }
1453 }
1454 };
1455
1456 let function_id =
1458 match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
1459 Ok(id) => id,
1460 Err(_e) => {
1461 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1462 let error = output::JsonError::function_not_found(&args.function);
1463 let wrapper = output::JsonResponse::new(error);
1464 println!("{}", wrapper.to_json());
1465 std::process::exit(output::EXIT_DATABASE);
1466 } else {
1467 output::error(&format!(
1468 "Function '{}' not found in database",
1469 args.function
1470 ));
1471 output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
1472 std::process::exit(output::EXIT_DATABASE);
1473 }
1474 }
1475 };
1476
1477 let cfg = match load_cfg_from_db(&db, function_id) {
1479 Ok(cfg) => cfg,
1480 Err(_e) => {
1481 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1482 let error = output::JsonError::new(
1483 "CgfLoadError",
1484 &format!("Failed to load CFG for function '{}'", args.function),
1485 output::E_CFG_ERROR,
1486 );
1487 let wrapper = output::JsonResponse::new(error);
1488 println!("{}", wrapper.to_json());
1489 std::process::exit(output::EXIT_DATABASE);
1490 } else {
1491 output::error(&format!(
1492 "Failed to load CFG for function '{}'",
1493 args.function
1494 ));
1495 output::info("The function may be corrupted. Try re-running 'magellan watch'");
1496 std::process::exit(output::EXIT_DATABASE);
1497 }
1498 }
1499 };
1500
1501 let coverage: Option<std::collections::HashMap<i64, i64>> =
1503 db.conn().ok().and_then(|conn| {
1504 let sql = "SELECT block_id, hit_count FROM cfg_block_coverage \
1505 WHERE block_id IN (SELECT id FROM cfg_blocks WHERE function_id = ?1)";
1506 let mut stmt = conn.prepare(sql).ok()?;
1507 let rows = stmt.query_map([function_id], |row| {
1508 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
1509 });
1510 let mut map = std::collections::HashMap::new();
1511 if let Ok(iter) = rows {
1512 for item in iter {
1513 if let Ok((block_id, hit_count)) = item {
1514 map.insert(block_id, hit_count);
1515 }
1516 }
1517 }
1518 if map.is_empty() {
1519 None
1520 } else {
1521 Some(map)
1522 }
1523 });
1524
1525 let format = args.format.unwrap_or(match cli.output {
1527 OutputFormat::Human => CfgFormat::Human,
1528 OutputFormat::Json => CfgFormat::Json,
1529 OutputFormat::Pretty => CfgFormat::Json,
1530 });
1531
1532 match format {
1533 CfgFormat::Human | CfgFormat::Dot => {
1534 let dot = export_dot(&cfg);
1536 println!("{}", dot);
1537 }
1538 CfgFormat::Json => {
1539 let export: CFGExport = export_json(&cfg, &args.function, coverage.as_ref());
1541 let response = output::JsonResponse::new(export);
1542
1543 match cli.output {
1544 OutputFormat::Json => println!("{}", response.to_json()),
1545 OutputFormat::Pretty => println!("{}", response.to_pretty_json()),
1546 OutputFormat::Human => println!("{}", response.to_pretty_json()),
1547 }
1548 }
1549 }
1550
1551 Ok(())
1552 }
1553
1554 pub(crate) fn create_test_cfg() -> crate::cfg::Cfg {
1559 use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
1560 use petgraph::graph::DiGraph;
1561 let mut g = DiGraph::new();
1562
1563 let b0 = g.add_node(BasicBlock {
1564 id: 0,
1565 db_id: None,
1566 kind: BlockKind::Entry,
1567 statements: vec!["let x = 1".to_string()],
1568 terminator: Terminator::Goto { target: 1 },
1569 source_location: None,
1570 coord_x: 0,
1571 coord_y: 0,
1572 coord_z: 0,
1573 });
1574
1575 let b1 = g.add_node(BasicBlock {
1576 id: 1,
1577 db_id: None,
1578 kind: BlockKind::Normal,
1579 statements: vec!["if x > 0".to_string()],
1580 terminator: Terminator::SwitchInt {
1581 targets: vec![2],
1582 otherwise: 3,
1583 },
1584 source_location: None,
1585 coord_x: 1,
1586 coord_y: 0,
1587 coord_z: 1,
1588 });
1589
1590 let b2 = g.add_node(BasicBlock {
1591 id: 2,
1592 db_id: None,
1593 kind: BlockKind::Exit,
1594 statements: vec!["return true".to_string()],
1595 terminator: Terminator::Return,
1596 source_location: None,
1597 coord_x: 2,
1598 coord_y: 0,
1599 coord_z: 2,
1600 });
1601
1602 let b3 = g.add_node(BasicBlock {
1603 id: 3,
1604 db_id: None,
1605 kind: BlockKind::Exit,
1606 statements: vec!["return false".to_string()],
1607 terminator: Terminator::Return,
1608 source_location: None,
1609 coord_x: 2,
1610 coord_y: 0,
1611 coord_z: 3,
1612 });
1613
1614 g.add_edge(b0, b1, EdgeType::Fallthrough);
1615 g.add_edge(b1, b2, EdgeType::TrueBranch);
1616 g.add_edge(b1, b3, EdgeType::FalseBranch);
1617
1618 g
1619 }
1620
1621 pub fn dominators(args: &DominatorsArgs, cli: &Cli) -> Result<()> {
1622 use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
1623 use crate::cfg::{DominatorTree, PostDominatorTree};
1624 use crate::storage::MirageDb;
1625
1626 let db_path = super::resolve_db_path(cli.db.clone())?;
1628
1629 if args.inter_procedural {
1631 return inter_procedural_dominators(args, cli, &db_path);
1632 }
1633
1634 let db = match MirageDb::open(&db_path) {
1636 Ok(db) => db,
1637 Err(_e) => {
1638 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1640 let error = output::JsonError::database_not_found(&db_path);
1641 let wrapper = output::JsonResponse::new(error);
1642 println!("{}", wrapper.to_json());
1643 std::process::exit(output::EXIT_DATABASE);
1644 } else {
1645 output::error(&format!("Failed to open database: {}", db_path));
1646 output::info("Hint: Run 'magellan watch' to create the database");
1647 std::process::exit(output::EXIT_DATABASE);
1648 }
1649 }
1650 };
1651
1652 let function_id =
1654 match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
1655 Ok(id) => id,
1656 Err(_e) => {
1657 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1658 let error = output::JsonError::function_not_found(&args.function);
1659 let wrapper = output::JsonResponse::new(error);
1660 println!("{}", wrapper.to_json());
1661 std::process::exit(output::EXIT_DATABASE);
1662 } else {
1663 output::error(&format!(
1664 "Function '{}' not found in database",
1665 args.function
1666 ));
1667 output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
1668 std::process::exit(output::EXIT_DATABASE);
1669 }
1670 }
1671 };
1672
1673 let cfg = match load_cfg_from_db(&db, function_id) {
1675 Ok(cfg) => cfg,
1676 Err(_e) => {
1677 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1678 let error = output::JsonError::new(
1679 "CgfLoadError",
1680 &format!("Failed to load CFG for function '{}'", args.function),
1681 output::E_CFG_ERROR,
1682 );
1683 let wrapper = output::JsonResponse::new(error);
1684 println!("{}", wrapper.to_json());
1685 std::process::exit(output::EXIT_DATABASE);
1686 } else {
1687 output::error(&format!(
1688 "Failed to load CFG for function '{}'",
1689 args.function
1690 ));
1691 output::info("The function may be corrupted. Try re-running 'magellan watch'");
1692 std::process::exit(output::EXIT_DATABASE);
1693 }
1694 }
1695 };
1696
1697 if args.post {
1699 let post_dom_tree = match PostDominatorTree::new(&cfg) {
1701 Some(tree) => tree,
1702 None => {
1703 output::error(
1704 "Could not compute post-dominator tree (CFG may have no exit blocks)",
1705 );
1706 std::process::exit(1);
1707 }
1708 };
1709
1710 if let Some(ref block_id_str) = args.must_pass_through {
1712 match block_id_str.parse::<usize>() {
1713 Ok(block_id) => {
1714 let target_node = cfg.node_indices().find(|&n| cfg[n].id == block_id);
1716
1717 let target_node = match target_node {
1718 Some(node) => node,
1719 None => {
1720 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1721 let error = output::JsonError::block_not_found(block_id);
1722 let wrapper = output::JsonResponse::new(error);
1723 println!("{}", wrapper.to_json());
1724 std::process::exit(1);
1725 } else {
1726 output::error(&format!("Block {} not found in CFG", block_id));
1727 std::process::exit(1);
1728 }
1729 }
1730 };
1731
1732 let must_pass: Vec<usize> = cfg
1734 .node_indices()
1735 .filter(|&n| post_dom_tree.post_dominates(target_node, n))
1736 .map(|n| cfg[n].id)
1737 .collect();
1738
1739 match cli.output {
1741 OutputFormat::Human => {
1742 println!("Function: {}", args.function);
1743 println!(
1744 "Post-Dominator Query: Blocks post-dominated by {}",
1745 block_id
1746 );
1747 println!("Count: {}", must_pass.len());
1748 println!();
1749 if must_pass.is_empty() {
1750 output::info("No blocks are post-dominated by this block");
1751 } else {
1752 println!("Blocks that must pass through {}:", block_id);
1753 for id in &must_pass {
1754 println!(" - Block {}", id);
1755 }
1756 }
1757 }
1758 OutputFormat::Json | OutputFormat::Pretty => {
1759 let response = DominanceResponse {
1760 function: args.function.clone(),
1761 kind: "post-dominators".to_string(),
1762 root: Some(cfg[post_dom_tree.root()].id),
1763 dominance_tree: vec![],
1764 must_pass_through: Some(MustPassThroughResult {
1765 block: block_id,
1766 must_pass,
1767 }),
1768 };
1769 let wrapper = output::JsonResponse::new(response);
1770 match cli.output {
1771 OutputFormat::Json => println!("{}", wrapper.to_json()),
1772 OutputFormat::Pretty => {
1773 println!("{}", wrapper.to_pretty_json())
1774 }
1775 _ => unreachable!(),
1776 }
1777 }
1778 }
1779 return Ok(());
1780 }
1781 Err(_) => {
1782 output::error(&format!("Invalid block ID: {}", block_id_str));
1783 std::process::exit(1);
1784 }
1785 }
1786 }
1787
1788 let dominance_tree: Vec<DominatorEntry> = cfg
1790 .node_indices()
1791 .map(|node| {
1792 let block = cfg[node].id;
1793 let immediate_dominator = post_dom_tree
1794 .immediate_post_dominator(node)
1795 .map(|n| cfg[n].id);
1796 let dominated: Vec<usize> = post_dom_tree
1797 .children(node)
1798 .iter()
1799 .map(|&n| cfg[n].id)
1800 .collect();
1801 DominatorEntry {
1802 block,
1803 immediate_dominator,
1804 dominated,
1805 }
1806 })
1807 .collect();
1808
1809 match cli.output {
1811 OutputFormat::Human => {
1812 println!("Function: {}", args.function);
1813 println!(
1814 "Post-Dominator Tree (root: {})",
1815 cfg[post_dom_tree.root()].id
1816 );
1817 println!();
1818
1819 print_dominator_tree_human(
1821 &cfg,
1822 post_dom_tree.as_dominator_tree(),
1823 post_dom_tree.root(),
1824 0,
1825 true,
1826 );
1827 }
1828 OutputFormat::Json | OutputFormat::Pretty => {
1829 let response = DominanceResponse {
1830 function: args.function.clone(),
1831 kind: "post-dominators".to_string(),
1832 root: Some(cfg[post_dom_tree.root()].id),
1833 dominance_tree,
1834 must_pass_through: None,
1835 };
1836 let wrapper = output::JsonResponse::new(response);
1837 match cli.output {
1838 OutputFormat::Json => println!("{}", wrapper.to_json()),
1839 OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
1840 _ => unreachable!(),
1841 }
1842 }
1843 }
1844 } else {
1845 let dom_tree = match DominatorTree::new(&cfg) {
1847 Some(tree) => tree,
1848 None => {
1849 output::error("Could not compute dominator tree (CFG may have no entry block)");
1850 std::process::exit(1);
1851 }
1852 };
1853
1854 if let Some(ref block_id_str) = args.must_pass_through {
1856 match block_id_str.parse::<usize>() {
1857 Ok(block_id) => {
1858 let target_node = cfg.node_indices().find(|&n| cfg[n].id == block_id);
1860
1861 let target_node = match target_node {
1862 Some(node) => node,
1863 None => {
1864 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1865 let error = output::JsonError::block_not_found(block_id);
1866 let wrapper = output::JsonResponse::new(error);
1867 println!("{}", wrapper.to_json());
1868 std::process::exit(1);
1869 } else {
1870 output::error(&format!("Block {} not found in CFG", block_id));
1871 std::process::exit(1);
1872 }
1873 }
1874 };
1875
1876 let must_pass: Vec<usize> = cfg
1878 .node_indices()
1879 .filter(|&n| dom_tree.dominates(target_node, n))
1880 .map(|n| cfg[n].id)
1881 .collect();
1882
1883 match cli.output {
1885 OutputFormat::Human => {
1886 println!("Function: {}", args.function);
1887 println!("Dominator Query: Blocks dominated by {}", block_id);
1888 println!("Count: {}", must_pass.len());
1889 println!();
1890 if must_pass.is_empty() {
1891 output::info("No blocks are dominated by this block");
1892 } else {
1893 println!("Blocks that must pass through {}:", block_id);
1894 for id in &must_pass {
1895 println!(" - Block {}", id);
1896 }
1897 }
1898 }
1899 OutputFormat::Json | OutputFormat::Pretty => {
1900 let response = DominanceResponse {
1901 function: args.function.clone(),
1902 kind: "dominators".to_string(),
1903 root: Some(cfg[dom_tree.root()].id),
1904 dominance_tree: vec![],
1905 must_pass_through: Some(MustPassThroughResult {
1906 block: block_id,
1907 must_pass,
1908 }),
1909 };
1910 let wrapper = output::JsonResponse::new(response);
1911 match cli.output {
1912 OutputFormat::Json => println!("{}", wrapper.to_json()),
1913 OutputFormat::Pretty => {
1914 println!("{}", wrapper.to_pretty_json())
1915 }
1916 _ => unreachable!(),
1917 }
1918 }
1919 }
1920 return Ok(());
1921 }
1922 Err(_) => {
1923 output::error(&format!("Invalid block ID: {}", block_id_str));
1924 std::process::exit(1);
1925 }
1926 }
1927 }
1928
1929 let dominance_tree: Vec<DominatorEntry> = cfg
1931 .node_indices()
1932 .map(|node| {
1933 let block = cfg[node].id;
1934 let immediate_dominator = dom_tree.immediate_dominator(node).map(|n| cfg[n].id);
1935 let dominated: Vec<usize> =
1936 dom_tree.children(node).iter().map(|&n| cfg[n].id).collect();
1937 DominatorEntry {
1938 block,
1939 immediate_dominator,
1940 dominated,
1941 }
1942 })
1943 .collect();
1944
1945 match cli.output {
1947 OutputFormat::Human => {
1948 println!("Function: {}", args.function);
1949 println!("Dominator Tree (root: {})", cfg[dom_tree.root()].id);
1950 println!();
1951
1952 print_dominator_tree_human(&cfg, &dom_tree, dom_tree.root(), 0, false);
1954 }
1955 OutputFormat::Json | OutputFormat::Pretty => {
1956 let response = DominanceResponse {
1957 function: args.function.clone(),
1958 kind: "dominators".to_string(),
1959 root: Some(cfg[dom_tree.root()].id),
1960 dominance_tree,
1961 must_pass_through: None,
1962 };
1963 let wrapper = output::JsonResponse::new(response);
1964 match cli.output {
1965 OutputFormat::Json => println!("{}", wrapper.to_json()),
1966 OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
1967 _ => unreachable!(),
1968 }
1969 }
1970 }
1971 }
1972
1973 Ok(())
1974 }
1975
1976 fn print_dominator_tree_human(
1978 cfg: &crate::cfg::Cfg,
1979 dom_tree: &crate::cfg::DominatorTree,
1980 node: petgraph::graph::NodeIndex,
1981 depth: usize,
1982 is_post: bool,
1983 ) {
1984 let indent = " ".repeat(depth);
1985 let block_id = cfg[node].id;
1986 let kind_label = if is_post {
1987 "post-dominator"
1988 } else {
1989 "dominator"
1990 };
1991
1992 println!("{}Block {} ({})", indent, block_id, kind_label);
1993
1994 for &child in dom_tree.children(node) {
1995 print_dominator_tree_human(cfg, dom_tree, child, depth + 1, is_post);
1996 }
1997 }
1998
1999 fn print_post_dominator_tree_human(
2001 cfg: &crate::cfg::Cfg,
2002 post_dom_tree: &crate::cfg::PostDominatorTree,
2003 node: petgraph::graph::NodeIndex,
2004 depth: usize,
2005 ) {
2006 let indent = " ".repeat(depth);
2007 let block_id = cfg[node].id;
2008
2009 println!("{}Block {} (post-dominator)", indent, block_id);
2010
2011 for &child in post_dom_tree.children(node) {
2012 print_post_dominator_tree_human(cfg, post_dom_tree, child, depth + 1);
2013 }
2014 }
2015
2016 fn inter_procedural_dominators(args: &DominatorsArgs, cli: &Cli, db_path: &str) -> Result<()> {
2021 use crate::analysis::MagellanBridge;
2022 use std::collections::{HashMap, HashSet};
2023
2024 let bridge = match MagellanBridge::open(db_path) {
2026 Ok(b) => b,
2027 Err(e) => {
2028 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2029 let error = output::JsonError::new(
2030 "MagellanUnavailable",
2031 &format!("Magellan database not available: {}", e),
2032 "Run 'magellan watch' to build the call graph",
2033 );
2034 let wrapper = output::JsonResponse::new(error);
2035 println!("{}", wrapper.to_json());
2036 std::process::exit(output::EXIT_DATABASE);
2037 } else {
2038 output::error(&format!("Magellan database not available: {}", e));
2039 output::info("Hint: Run 'magellan watch' to build the call graph");
2040 std::process::exit(output::EXIT_DATABASE);
2041 }
2042 }
2043 };
2044
2045 let condensed = match bridge.condense_call_graph() {
2047 Ok(c) => c,
2048 Err(e) => {
2049 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2050 let error = output::JsonError::new(
2051 "CondensationError",
2052 &format!("Failed to condense call graph: {}", e),
2053 "Ensure the call graph is properly built",
2054 );
2055 let wrapper = output::JsonResponse::new(error);
2056 println!("{}", wrapper.to_json());
2057 std::process::exit(output::EXIT_DATABASE);
2058 } else {
2059 output::error(&format!("Failed to condense call graph: {}", e));
2060 output::info("Hint: Ensure the call graph is properly built");
2061 std::process::exit(output::EXIT_DATABASE);
2062 }
2063 }
2064 };
2065
2066 let mut adjacency: HashMap<i64, Vec<i64>> = HashMap::new();
2068 for &(from_id, to_id) in &condensed.graph.edges {
2069 adjacency.entry(from_id).or_default().push(to_id);
2070 }
2071
2072 let mut symbol_to_scc: HashMap<String, i64> = HashMap::new();
2074 let mut scc_members: HashMap<i64, Vec<String>> = HashMap::new();
2075
2076 for supernode in &condensed.graph.supernodes {
2077 let scc_id = supernode.id;
2078 for member in &supernode.members {
2079 if let Some(fqn) = &member.fqn {
2080 symbol_to_scc.insert(fqn.clone(), scc_id);
2081 scc_members.entry(scc_id).or_default().push(fqn.clone());
2082 }
2083 }
2084 }
2085
2086 let mut dominating_functions: Vec<String> = Vec::new();
2089
2090 if let Some(&target_scc_id) = symbol_to_scc.get(&args.function) {
2091 for (&scc_id, _) in &scc_members {
2093 if scc_id != target_scc_id {
2094 let mut visited = HashSet::new();
2095 if can_reach_scc(scc_id, target_scc_id, &adjacency, &mut visited) {
2096 if let Some(members) = scc_members.get(&scc_id) {
2098 dominating_functions.extend(members.clone());
2099 }
2100 }
2101 }
2102 }
2103 }
2104
2105 dominating_functions.sort();
2107
2108 match cli.output {
2110 OutputFormat::Human => {
2111 output::header(&format!("Inter-procedural Dominators: {}", args.function));
2112 output::info("Functions that must execute before this function can be reached");
2113 println!();
2114
2115 if dominating_functions.is_empty() {
2116 println!(
2117 "No dominators found (this may be an entry point or not in call graph)"
2118 );
2119 } else {
2120 println!(
2121 "Found {} dominating function(s):",
2122 dominating_functions.len()
2123 );
2124 println!();
2125 for (i, dominator) in dominating_functions.iter().enumerate() {
2126 println!("{}. {}", i + 1, dominator);
2127 }
2128 println!();
2129 output::info("These functions are on all call paths to the target");
2130 }
2131 }
2132 OutputFormat::Json => {
2133 let response = InterProceduralDominanceResponse {
2134 function: args.function.clone(),
2135 kind: "inter-procedural-dominators".to_string(),
2136 dominator_count: dominating_functions.len(),
2137 dominators: dominating_functions.clone(),
2138 };
2139 let wrapper = output::JsonResponse::new(response);
2140 println!("{}", wrapper.to_json());
2141 }
2142 OutputFormat::Pretty => {
2143 let response = InterProceduralDominanceResponse {
2144 function: args.function.clone(),
2145 kind: "inter-procedural-dominators".to_string(),
2146 dominator_count: dominating_functions.len(),
2147 dominators: dominating_functions.clone(),
2148 };
2149 let wrapper = output::JsonResponse::new(response);
2150 println!("{}", wrapper.to_pretty_json());
2151 }
2152 }
2153
2154 Ok(())
2155 }
2156
2157 fn can_reach_scc(
2159 from: i64,
2160 to: i64,
2161 adjacency: &std::collections::HashMap<i64, Vec<i64>>,
2162 visited: &mut std::collections::HashSet<i64>,
2163 ) -> bool {
2164 if from == to {
2165 return true;
2166 }
2167 if visited.contains(&from) {
2168 return false;
2169 }
2170 visited.insert(from);
2171
2172 if let Some(neighbors) = adjacency.get(&from) {
2173 for &neighbor in neighbors {
2174 if can_reach_scc(neighbor, to, adjacency, visited) {
2175 return true;
2176 }
2177 }
2178 }
2179 false
2180 }
2181
2182 pub fn loops(args: &LoopsArgs, cli: &Cli) -> Result<()> {
2183 use crate::cfg::detect_natural_loops;
2184 use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
2185 use crate::storage::MirageDb;
2186
2187 let db_path = super::resolve_db_path(cli.db.clone())?;
2189
2190 let db = match MirageDb::open(&db_path) {
2192 Ok(db) => db,
2193 Err(_e) => {
2194 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2196 let error = output::JsonError::database_not_found(&db_path);
2197 let wrapper = output::JsonResponse::new(error);
2198 println!("{}", wrapper.to_json());
2199 std::process::exit(output::EXIT_DATABASE);
2200 } else {
2201 output::error(&format!("Failed to open database: {}", db_path));
2202 output::info("Hint: Run 'magellan watch' to create the database");
2203 std::process::exit(output::EXIT_DATABASE);
2204 }
2205 }
2206 };
2207
2208 let function_id =
2210 match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
2211 Ok(id) => id,
2212 Err(_e) => {
2213 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2214 let error = output::JsonError::function_not_found(&args.function);
2215 let wrapper = output::JsonResponse::new(error);
2216 println!("{}", wrapper.to_json());
2217 std::process::exit(output::EXIT_DATABASE);
2218 } else {
2219 output::error(&format!(
2220 "Function '{}' not found in database",
2221 args.function
2222 ));
2223 output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
2224 std::process::exit(output::EXIT_DATABASE);
2225 }
2226 }
2227 };
2228
2229 let cfg = match load_cfg_from_db(&db, function_id) {
2231 Ok(cfg) => cfg,
2232 Err(_e) => {
2233 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2234 let error = output::JsonError::new(
2235 "CgfLoadError",
2236 &format!("Failed to load CFG for function '{}'", args.function),
2237 output::E_CFG_ERROR,
2238 );
2239 let wrapper = output::JsonResponse::new(error);
2240 println!("{}", wrapper.to_json());
2241 std::process::exit(output::EXIT_DATABASE);
2242 } else {
2243 output::error(&format!(
2244 "Failed to load CFG for function '{}'",
2245 args.function
2246 ));
2247 output::info("The function may be corrupted. Try re-running 'magellan watch'");
2248 std::process::exit(output::EXIT_DATABASE);
2249 }
2250 }
2251 };
2252
2253 let natural_loops = detect_natural_loops(&cfg);
2255
2256 let loop_infos: Vec<LoopInfo> = natural_loops
2258 .iter()
2259 .map(|loop_| {
2260 let nesting_level = loop_.nesting_level(&natural_loops);
2261 let body_blocks: Vec<usize> = loop_.body.iter().map(|&node| cfg[node].id).collect();
2262 LoopInfo {
2263 header: cfg[loop_.header].id,
2264 back_edge_from: cfg[loop_.back_edge.0].id,
2265 body_size: loop_.size(),
2266 nesting_level,
2267 body_blocks,
2268 }
2269 })
2270 .collect();
2271
2272 match cli.output {
2274 OutputFormat::Human => {
2275 println!("Function: {}", args.function);
2276 println!("Natural Loops: {}", natural_loops.len());
2277 println!();
2278
2279 if natural_loops.is_empty() {
2280 output::info("No natural loops detected in this function");
2281 } else {
2282 for (i, loop_info) in loop_infos.iter().enumerate() {
2283 println!("Loop {}:", i + 1);
2284 println!(" Header: Block {}", loop_info.header);
2285 println!(" Back edge from: Block {}", loop_info.back_edge_from);
2286 println!(" Body size: {} blocks", loop_info.body_size);
2287 println!(" Nesting level: {}", loop_info.nesting_level);
2288
2289 if args.verbose {
2290 println!(" Body blocks: {:?}", loop_info.body_blocks);
2291 }
2292 println!();
2293 }
2294 }
2295 }
2296 OutputFormat::Json | OutputFormat::Pretty => {
2297 let response = LoopsResponse {
2298 function: args.function.clone(),
2299 loop_count: natural_loops.len(),
2300 loops: loop_infos,
2301 };
2302 let wrapper = output::JsonResponse::new(response);
2303 match cli.output {
2304 OutputFormat::Json => println!("{}", wrapper.to_json()),
2305 OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
2306 _ => unreachable!(),
2307 }
2308 }
2309 }
2310
2311 Ok(())
2312 }
2313
2314 pub fn unreachable(args: &UnreachableArgs, cli: &Cli) -> Result<()> {
2315 use crate::analysis::DeadSymbolJson;
2316 use crate::analysis::MagellanBridge;
2317 use crate::cfg::load_cfg_from_db;
2318 use crate::cfg::reachability::find_unreachable;
2319 use crate::storage::MirageDb;
2320 use petgraph::visit::EdgeRef;
2321
2322 let db_path = super::resolve_db_path(cli.db.clone())?;
2324
2325 let uncalled_functions: Option<Vec<DeadSymbolJson>> = if args.include_uncalled {
2327 match MagellanBridge::open(&db_path) {
2328 Ok(bridge) => {
2329 match bridge.dead_symbols("main") {
2330 Ok(dead) => {
2331 let json_symbols: Vec<DeadSymbolJson> =
2332 dead.iter().map(|d| d.into()).collect();
2333 Some(json_symbols)
2334 }
2335 Err(e) => {
2336 eprintln!("Warning: Failed to detect uncalled functions: {}", e);
2338 None
2339 }
2340 }
2341 }
2342 Err(e) => {
2343 eprintln!(
2345 "Warning: Could not open Magellan database for --include-uncalled: {}",
2346 e
2347 );
2348 eprintln!("Note: --include-uncalled requires a Magellan code graph database");
2349 None
2350 }
2351 }
2352 } else {
2353 None
2354 };
2355
2356 let db = match MirageDb::open(&db_path) {
2358 Ok(db) => db,
2359 Err(_e) => {
2360 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2362 let error = output::JsonError::database_not_found(&db_path);
2363 let wrapper = output::JsonResponse::new(error);
2364 println!("{}", wrapper.to_json());
2365 std::process::exit(output::EXIT_DATABASE);
2366 } else {
2367 output::error(&format!("Failed to open database: {}", db_path));
2368 output::info("Hint: Run 'magellan watch' to create the database");
2369 std::process::exit(output::EXIT_DATABASE);
2370 }
2371 }
2372 };
2373
2374 struct FunctionUnreachable {
2376 function_name: String,
2377 function_id: i64,
2378 blocks: Vec<UnreachableBlock>,
2379 }
2380
2381 if !db.is_sqlite() {
2384 output::error("The 'unreachable' command currently requires SQLite backend.");
2385 output::info("Use SQLite backend or run with --help for alternatives.");
2386 std::process::exit(output::EXIT_USAGE);
2387 }
2388
2389 let mut function_rows: Vec<(String, i64)> = Vec::new();
2391 let mut stmt = match db.conn()?.prepare(
2393 "SELECT name, id FROM graph_entities WHERE kind = 'Symbol' AND json_extract(data, '$.kind') = 'Function'") {
2394 Ok(stmt) => stmt,
2395 Err(e) => {
2396 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2397 let error = output::JsonError::new(
2398 "QueryError",
2399 &format!("Failed to query functions: {}", e),
2400 output::E_DATABASE_NOT_FOUND,
2401 );
2402 let wrapper = output::JsonResponse::new(error);
2403 println!("{}", wrapper.to_json());
2404 std::process::exit(output::EXIT_DATABASE);
2405 } else {
2406 output::error(&format!("Failed to query functions: {}", e));
2407 std::process::exit(output::EXIT_DATABASE);
2408 }
2409 }
2410 };
2411
2412 let rows_result = stmt.query_map([], |row| {
2413 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
2414 });
2415
2416 match rows_result {
2417 Ok(rows) => {
2418 for row in rows {
2419 match row {
2420 Ok((name, id)) => function_rows.push((name, id)),
2421 Err(e) => {
2422 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2423 let error = output::JsonError::new(
2424 "QueryError",
2425 &format!("Failed to read function row: {}", e),
2426 output::E_DATABASE_NOT_FOUND,
2427 );
2428 let wrapper = output::JsonResponse::new(error);
2429 println!("{}", wrapper.to_json());
2430 std::process::exit(output::EXIT_DATABASE);
2431 } else {
2432 output::error(&format!("Failed to read function row: {}", e));
2433 std::process::exit(output::EXIT_DATABASE);
2434 }
2435 }
2436 }
2437 }
2438 }
2439 Err(e) => {
2440 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2441 let error = output::JsonError::new(
2442 "QueryError",
2443 &format!("Failed to execute query: {}", e),
2444 output::E_DATABASE_NOT_FOUND,
2445 );
2446 let wrapper = output::JsonResponse::new(error);
2447 println!("{}", wrapper.to_json());
2448 std::process::exit(output::EXIT_DATABASE);
2449 } else {
2450 output::error(&format!("Failed to execute query: {}", e));
2451 std::process::exit(output::EXIT_DATABASE);
2452 }
2453 }
2454 }
2455
2456 let mut all_results = Vec::new();
2458 for (function_name, function_id) in function_rows {
2459 match load_cfg_from_db(&db, function_id) {
2460 Ok(cfg) => {
2461 let unreachable_indices = find_unreachable(&cfg);
2462 if !unreachable_indices.is_empty() {
2463 let blocks: Vec<UnreachableBlock> = unreachable_indices
2464 .iter()
2465 .map(|&idx| {
2466 let block = &cfg[idx];
2467 let kind_str = format!("{:?}", block.kind);
2468 let terminator_str = format!("{:?}", block.terminator);
2469
2470 let incoming_edges = if args.show_branches {
2471 cfg.edge_references()
2472 .filter(|edge| edge.target() == idx)
2473 .filter_map(|edge| {
2474 let source_block = &cfg[edge.source()];
2475 cfg.edge_weight(edge.id()).map(|edge_type| {
2476 IncomingEdge {
2477 from_block: source_block.id,
2478 edge_type: format!("{:?}", edge_type),
2479 }
2480 })
2481 })
2482 .collect()
2483 } else {
2484 vec![]
2485 };
2486
2487 UnreachableBlock {
2488 block_id: block.id,
2489 kind: kind_str,
2490 statements: block.statements.clone(),
2491 terminator: terminator_str,
2492 incoming_edges,
2493 }
2494 })
2495 .collect();
2496
2497 all_results.push(FunctionUnreachable {
2498 function_name,
2499 function_id,
2500 blocks,
2501 });
2502 }
2503 }
2504 Err(_) => {
2505 continue;
2507 }
2508 }
2509 }
2510
2511 let total_functions = all_results.len();
2513 let functions_with_unreachable =
2514 all_results.iter().filter(|r| !r.blocks.is_empty()).count();
2515 let total_blocks: usize = all_results.iter().map(|r| r.blocks.len()).sum();
2516
2517 match cli.output {
2519 OutputFormat::Human => {
2520 if let Some(ref uncalled) = uncalled_functions {
2522 println!("Uncalled Functions ({}):", uncalled.len());
2523 for dead in uncalled {
2524 let name = dead.fqn.as_deref().unwrap_or("?");
2525 println!(" - {} ({})", name, dead.kind);
2526 println!(" File: {}", dead.file_path);
2527 println!(" Reason: {}", dead.reason);
2528 }
2529 println!();
2530 }
2531
2532 if total_blocks == 0 {
2534 if uncalled_functions.is_none()
2535 || uncalled_functions
2536 .as_ref()
2537 .map(|v| v.is_empty())
2538 .unwrap_or(false)
2539 {
2540 output::info("No unreachable code found");
2541 }
2542 return Ok(());
2543 }
2544
2545 println!("Unreachable Code Blocks:");
2546 println!(" Total blocks: {}", total_blocks);
2547 println!(
2548 " Functions with unreachable: {}/{}",
2549 functions_with_unreachable, total_functions
2550 );
2551 println!();
2552
2553 for result in &all_results {
2554 if result.blocks.is_empty() {
2555 continue;
2556 }
2557
2558 println!("Function: {}", result.function_name);
2559
2560 for block in &result.blocks {
2561 println!(" Block {} ({})", block.block_id, block.kind);
2562 if !block.statements.is_empty() {
2563 for stmt in &block.statements {
2564 println!(" - {}", stmt);
2565 }
2566 }
2567 println!(" Terminator: {}", block.terminator);
2568 println!();
2569 }
2570
2571 if args.show_branches {
2572 println!(" Incoming Edges:");
2573 for block in &result.blocks {
2574 if block.incoming_edges.is_empty() {
2575 println!(
2576 " Block {} has no incoming edges (entry or isolated)",
2577 block.block_id
2578 );
2579 } else {
2580 println!(" Block {} incoming edges:", block.block_id);
2581 for edge in &block.incoming_edges {
2582 println!(
2583 " from block {} ({})",
2584 edge.from_block, edge.edge_type
2585 );
2586 }
2587 }
2588 }
2589 println!();
2590 }
2591 }
2592 }
2593 OutputFormat::Json | OutputFormat::Pretty => {
2594 let all_blocks: Vec<UnreachableBlock> =
2596 all_results.iter().flat_map(|r| r.blocks.clone()).collect();
2597
2598 let response = UnreachableResponse {
2599 function: "all".to_string(),
2600 total_functions,
2601 functions_with_unreachable,
2602 unreachable_count: total_blocks,
2603 blocks: all_blocks,
2604 uncalled_functions: uncalled_functions,
2605 };
2606 let wrapper = output::JsonResponse::new(response);
2607
2608 match cli.output {
2609 OutputFormat::Json => println!("{}", wrapper.to_json()),
2610 OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
2611 _ => {}
2612 }
2613 }
2614 }
2615
2616 Ok(())
2617 }
2618
2619 pub fn verify(args: &VerifyArgs, cli: &Cli) -> Result<()> {
2620 use crate::cfg::{enumerate_paths, load_cfg_from_db, PathLimits};
2621 use crate::storage::MirageDb;
2622 use rusqlite::OptionalExtension;
2623
2624 let db_path = super::resolve_db_path(cli.db.clone())?;
2626
2627 let db = match MirageDb::open(&db_path) {
2629 Ok(db) => db,
2630 Err(_e) => {
2631 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2633 let error = output::JsonError::database_not_found(&db_path);
2634 let wrapper = output::JsonResponse::new(error);
2635 println!("{}", wrapper.to_json());
2636 std::process::exit(output::EXIT_DATABASE);
2637 } else {
2638 output::error(&format!("Failed to open database: {}", db_path));
2639 output::info("Hint: Run 'magellan watch' to create the database");
2640 std::process::exit(output::EXIT_DATABASE);
2641 }
2642 }
2643 };
2644
2645 let path_id = &args.path_id;
2646
2647 if !db.is_sqlite() {
2649 let msg = "Path verification requires SQLite backend with path caching.";
2650 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2651 let error =
2652 output::JsonError::new("UnsupportedBackend", msg, output::E_INVALID_INPUT);
2653 let wrapper = output::JsonResponse::new(error);
2654 println!("{}", wrapper.to_json());
2655 std::process::exit(output::EXIT_USAGE);
2656 } else {
2657 output::error(msg);
2658 output::info("retired binary backend backend does not support path caching.");
2659 std::process::exit(output::EXIT_USAGE);
2660 }
2661 }
2662
2663 let cached_path_info: Option<(String, i64, String)> = db
2665 .conn()?
2666 .query_row(
2667 "SELECT path_id, function_id, path_kind FROM cfg_paths WHERE path_id = ?1",
2668 rusqlite::params![path_id],
2669 |row| {
2670 Ok((
2671 row.get::<_, String>(0)?,
2672 row.get::<_, i64>(1)?,
2673 row.get::<_, String>(2)?,
2674 ))
2675 },
2676 )
2677 .optional()
2678 .unwrap_or(None);
2679
2680 let (found_in_cache, function_id, _path_kind) = match cached_path_info {
2681 Some((_id, fid, kind)) => (true, fid, kind),
2682 None => {
2683 let result = VerifyResult {
2685 path_id: path_id.clone(),
2686 valid: false,
2687 found_in_cache: false,
2688 function_id: None,
2689 reason: "Path not found in cache".to_string(),
2690 current_paths: 0,
2691 };
2692
2693 match cli.output {
2694 OutputFormat::Human => {
2695 println!("Path ID {}: not found in cache", path_id);
2696 println!(" The path may have been invalidated or never existed.");
2697 }
2698 OutputFormat::Json | OutputFormat::Pretty => {
2699 let wrapper = output::JsonResponse::new(result);
2700 match cli.output {
2701 OutputFormat::Json => println!("{}", wrapper.to_json()),
2702 OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
2703 _ => unreachable!(),
2704 }
2705 }
2706 }
2707 return Ok(());
2708 }
2709 };
2710
2711 let cfg = match load_cfg_from_db(&db, function_id) {
2714 Ok(cfg) => cfg,
2715 Err(_e) => {
2716 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2717 let error = output::JsonError::new(
2718 "CgfLoadError",
2719 &format!("Failed to load CFG for function_id {}", function_id),
2720 output::E_CFG_ERROR,
2721 );
2722 let wrapper = output::JsonResponse::new(error);
2723 println!("{}", wrapper.to_json());
2724 std::process::exit(output::EXIT_DATABASE);
2725 } else {
2726 output::error(&format!(
2727 "Failed to load CFG for function_id {}",
2728 function_id
2729 ));
2730 output::info(
2731 "The function data may be corrupted. Try re-running 'magellan watch'",
2732 );
2733 std::process::exit(output::EXIT_DATABASE);
2734 }
2735 }
2736 };
2737
2738 let limits = PathLimits::default();
2740 let current_paths = enumerate_paths(&cfg, &limits);
2741 let current_path_count = current_paths.len();
2742
2743 let path_still_valid = current_paths.iter().any(|p| &p.path_id == path_id);
2745
2746 let reason = if path_still_valid {
2747 "Path found in current enumeration".to_string()
2748 } else {
2749 "Path no longer exists in current enumeration (code may have changed)".to_string()
2750 };
2751
2752 let result = VerifyResult {
2753 path_id: path_id.clone(),
2754 valid: path_still_valid,
2755 found_in_cache,
2756 function_id: Some(function_id),
2757 reason,
2758 current_paths: current_path_count,
2759 };
2760
2761 match cli.output {
2762 OutputFormat::Human => {
2763 println!(
2764 "Path ID {}: {}",
2765 path_id,
2766 if result.valid { "valid" } else { "invalid" }
2767 );
2768 println!(
2769 " Found in cache: {}",
2770 if found_in_cache { "yes" } else { "no" }
2771 );
2772 println!(" Status: {}", result.reason);
2773 println!(" Current total paths: {}", current_path_count);
2774 if !path_still_valid {
2775 println!();
2776 output::info("The path may have been invalidated by code changes.");
2777 output::info("Consider re-running path enumeration to update the cache.");
2778 }
2779 }
2780 OutputFormat::Json | OutputFormat::Pretty => {
2781 let wrapper = output::JsonResponse::new(result);
2782 match cli.output {
2783 OutputFormat::Json => println!("{}", wrapper.to_json()),
2784 OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
2785 _ => unreachable!(),
2786 }
2787 }
2788 }
2789
2790 Ok(())
2791 }
2792
2793 pub fn blast_zone(args: &BlastZoneArgs, cli: &Cli) -> Result<()> {
2794 use crate::cfg::{find_reachable_from_block, load_cfg_from_db, resolve_function_name};
2795 use crate::storage::{compute_path_impact_from_db, get_function_name_db, MirageDb};
2796 use rusqlite::OptionalExtension;
2797
2798 let db_path = super::resolve_db_path(cli.db.clone())?;
2800
2801 let db = match MirageDb::open(&db_path) {
2803 Ok(db) => db,
2804 Err(_e) => {
2805 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2806 let error = output::JsonError::database_not_found(&db_path);
2807 let wrapper = output::JsonResponse::new(error);
2808 println!("{}", wrapper.to_json());
2809 std::process::exit(output::EXIT_DATABASE);
2810 } else {
2811 output::error(&format!("Failed to open database: {}", db_path));
2812 output::info("Hint: Run 'magellan watch' to create the database");
2813 std::process::exit(output::EXIT_DATABASE);
2814 }
2815 }
2816 };
2817
2818 if let Some(ref path_id) = args.path_id {
2820 if !db.is_sqlite() {
2822 let msg = "Path-based blast-zone requires SQLite backend. Use block-based analysis with --function and --block-id instead.";
2823 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2824 let error =
2825 output::JsonError::new("UnsupportedBackend", msg, output::E_INVALID_INPUT);
2826 let wrapper = output::JsonResponse::new(error);
2827 println!("{}", wrapper.to_json());
2828 std::process::exit(output::EXIT_USAGE);
2829 } else {
2830 output::error(msg);
2831 output::info("retired binary backend backend does not support path caching. Use: mirage blast-zone --function <name> --block-id <id>");
2832 std::process::exit(output::EXIT_USAGE);
2833 }
2834 }
2835
2836 let path_id_trimmed = path_id.trim();
2838
2839 if path_id_trimmed.len() < 10 {
2841 let msg = format!("Invalid path_id format: '{}'", path_id_trimmed);
2842 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2843 let error =
2844 output::JsonError::new("InvalidInput", &msg, output::E_INVALID_INPUT);
2845 let wrapper = output::JsonResponse::new(error);
2846 println!("{}", wrapper.to_json());
2847 std::process::exit(output::EXIT_USAGE);
2848 } else {
2849 output::error(&msg);
2850 output::info("Path ID should be a BLAKE3 hash (64 hex characters)");
2851 std::process::exit(output::EXIT_USAGE);
2852 }
2853 }
2854
2855 let (function_id, path_kind): (i64, String) = match db
2857 .conn()?
2858 .query_row(
2859 "SELECT function_id, path_kind FROM cfg_paths WHERE path_id = ?1",
2860 rusqlite::params![path_id_trimmed],
2861 |row| Ok((row.get(0)?, row.get(1)?)),
2862 )
2863 .optional()
2864 {
2865 Ok(Some(data)) => data,
2866 Ok(None) => {
2867 let msg = format!("Path '{}' not found in cache", path_id_trimmed);
2868 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2869 let error =
2870 output::JsonError::new("PathNotFound", &msg, output::E_PATH_NOT_FOUND);
2871 let wrapper = output::JsonResponse::new(error);
2872 println!("{}", wrapper.to_json());
2873 std::process::exit(output::EXIT_FILE_NOT_FOUND);
2874 } else {
2875 output::error(&msg);
2876 output::info("Hint: Run 'mirage paths' to enumerate paths first");
2877 std::process::exit(output::EXIT_FILE_NOT_FOUND);
2878 }
2879 }
2880 Err(e) => {
2881 let msg = format!("Failed to query path: {}", e);
2882 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2883 let error = output::JsonError::new(
2884 "DatabaseError",
2885 &msg,
2886 output::E_DATABASE_NOT_FOUND,
2887 );
2888 let wrapper = output::JsonResponse::new(error);
2889 println!("{}", wrapper.to_json());
2890 std::process::exit(output::EXIT_DATABASE);
2891 } else {
2892 output::error(&msg);
2893 std::process::exit(output::EXIT_DATABASE);
2894 }
2895 }
2896 };
2897
2898 if !args.include_errors && path_kind == "error" {
2900 let msg = format!(
2901 "Path '{}' is an error path (use --include-errors to analyze)",
2902 path_id_trimmed
2903 );
2904 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2905 let error =
2906 output::JsonError::new("ErrorPathExcluded", &msg, output::E_INVALID_INPUT);
2907 let wrapper = output::JsonResponse::new(error);
2908 println!("{}", wrapper.to_json());
2909 std::process::exit(output::EXIT_USAGE);
2910 } else {
2911 output::error(&msg);
2912 output::info("Use --include-errors to include error paths in analysis");
2913 std::process::exit(output::EXIT_USAGE);
2914 }
2915 }
2916
2917 let cfg = match load_cfg_from_db(&db, function_id) {
2919 Ok(cfg) => cfg,
2920 Err(_e) => {
2921 let msg = format!("Failed to load CFG for function_id {}", function_id);
2922 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2923 let error =
2924 output::JsonError::new("CgfLoadError", &msg, output::E_CFG_ERROR);
2925 let wrapper = output::JsonResponse::new(error);
2926 println!("{}", wrapper.to_json());
2927 std::process::exit(output::EXIT_DATABASE);
2928 } else {
2929 output::error(&msg);
2930 output::info(
2931 "The function may be corrupted. Try re-running 'magellan watch'",
2932 );
2933 std::process::exit(output::EXIT_DATABASE);
2934 }
2935 }
2936 };
2937
2938 let function_name = get_function_name_db(&db, function_id)
2940 .unwrap_or_else(|| format!("<function_{}>", function_id));
2941
2942 let max_depth = if args.max_depth == 100 {
2944 None
2945 } else {
2946 Some(args.max_depth)
2947 };
2948 let impact =
2949 match compute_path_impact_from_db(db.conn()?, path_id_trimmed, &cfg, max_depth) {
2950 Ok(impact) => impact,
2951 Err(e) => {
2952 let msg = format!("Failed to compute path impact: {}", e);
2953 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2954 let error =
2955 output::JsonError::new("ImpactError", &msg, output::E_CFG_ERROR);
2956 let wrapper = output::JsonResponse::new(error);
2957 println!("{}", wrapper.to_json());
2958 std::process::exit(output::EXIT_ERROR);
2959 } else {
2960 output::error(&msg);
2961 std::process::exit(output::EXIT_ERROR);
2962 }
2963 }
2964 };
2965
2966 let (forward_impact, backward_impact): (
2968 Option<Vec<CallGraphSymbol>>,
2969 Option<Vec<CallGraphSymbol>>,
2970 ) = if args.use_call_graph {
2971 use crate::analysis::MagellanBridge;
2972 match MagellanBridge::open(&db_path) {
2973 Ok(bridge) => {
2974 let symbol_id = function_name.as_str();
2976 let forward: Option<Vec<CallGraphSymbol>> = bridge
2977 .reachable_symbols(symbol_id)
2978 .map(|symbols| {
2979 symbols
2980 .into_iter()
2981 .map(|s| CallGraphSymbol {
2982 symbol_id: s.symbol_id,
2983 fqn: s.fqn,
2984 file_path: s.file_path,
2985 kind: s.kind,
2986 })
2987 .collect()
2988 })
2989 .ok();
2990 let backward: Option<Vec<CallGraphSymbol>> = bridge
2991 .reverse_reachable_symbols(symbol_id)
2992 .map(|symbols| {
2993 symbols
2994 .into_iter()
2995 .map(|s| CallGraphSymbol {
2996 symbol_id: s.symbol_id,
2997 fqn: s.fqn,
2998 file_path: s.file_path,
2999 kind: s.kind,
3000 })
3001 .collect()
3002 })
3003 .ok();
3004 (forward, backward)
3005 }
3006 Err(e) => {
3007 eprintln!(
3008 "Warning: Could not open Magellan database for call graph analysis: {}",
3009 e
3010 );
3011 eprintln!("Note: --use-call-graph requires a Magellan code graph database");
3012 (None, None)
3013 }
3014 }
3015 } else {
3016 (None, None)
3017 };
3018
3019 match cli.output {
3021 OutputFormat::Human => {
3022 println!("Path Impact Analysis");
3023 println!();
3024 println!("Path ID: {}", impact.path_id);
3025 println!("Function: {}", function_name);
3026 println!("Path kind: {}", path_kind);
3027 println!("Path length: {} blocks", impact.path_length);
3028 println!();
3029
3030 if let Some(ref forward) = forward_impact {
3032 println!("Inter-Procedural Impact (Call Graph):");
3033 println!(" Forward Impact: {} functions reached", forward.len());
3034 for sym in forward {
3035 println!(" - {}", sym.fqn.as_deref().unwrap_or(&sym.file_path));
3036 }
3037 }
3038 if let Some(ref backward) = backward_impact {
3039 if !backward.is_empty() {
3040 println!(
3041 " Backward Impact: {} functions can reach this",
3042 backward.len()
3043 );
3044 for sym in backward {
3045 println!(" - {}", sym.fqn.as_deref().unwrap_or(&sym.file_path));
3046 }
3047 }
3048 }
3049 println!();
3050
3051 println!("Intra-Procedural Impact (CFG):");
3052 println!(" Unique blocks affected: {}", impact.impact_count);
3053 if impact.impact_count > 0 {
3054 println!(" Affected blocks: {:?}", impact.unique_blocks_affected);
3055 } else {
3056 println!(" Affected blocks: (none - path has no downstream impact)");
3057 }
3058 if let Some(depth) = max_depth {
3059 println!(" Max depth: {}", depth);
3060 } else {
3061 println!(" Max depth: unlimited");
3062 }
3063 }
3064 OutputFormat::Json | OutputFormat::Pretty => {
3065 let response = PathImpactResponse {
3066 path_id: impact.path_id.clone(),
3067 path_length: impact.path_length,
3068 unique_blocks_affected: impact.unique_blocks_affected,
3069 impact_count: impact.impact_count,
3070 forward_impact: forward_impact.clone(),
3071 backward_impact: backward_impact.clone(),
3072 };
3073 let wrapper = output::JsonResponse::new(response);
3074 match cli.output {
3075 OutputFormat::Json => println!("{}", wrapper.to_json()),
3076 OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
3077 _ => unreachable!(),
3078 }
3079 }
3080 }
3081 } else {
3082 let function_ref = args
3085 .function
3086 .as_ref()
3087 .expect("--function is required for block-based analysis");
3088
3089 let function_id = match resolve_function_name(&db, function_ref) {
3091 Ok(id) => id,
3092 Err(_e) => {
3093 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3094 let error = output::JsonError::function_not_found(function_ref);
3095 let wrapper = output::JsonResponse::new(error);
3096 println!("{}", wrapper.to_json());
3097 std::process::exit(output::EXIT_DATABASE);
3098 } else {
3099 output::error(&format!(
3100 "Function '{}' not found in database",
3101 function_ref
3102 ));
3103 output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
3104 std::process::exit(output::EXIT_DATABASE);
3105 }
3106 }
3107 };
3108
3109 let function_name = get_function_name_db(&db, function_id)
3111 .unwrap_or_else(|| format!("<function_{}>", function_id));
3112
3113 let cfg = match load_cfg_from_db(&db, function_id) {
3115 Ok(cfg) => cfg,
3116 Err(_e) => {
3117 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3118 let error = output::JsonError::new(
3119 "CgfLoadError",
3120 &format!("Failed to load CFG for function '{}'", function_ref),
3121 output::E_CFG_ERROR,
3122 );
3123 let wrapper = output::JsonResponse::new(error);
3124 println!("{}", wrapper.to_json());
3125 std::process::exit(output::EXIT_DATABASE);
3126 } else {
3127 output::error(&format!(
3128 "Failed to load CFG for function '{}'",
3129 function_ref
3130 ));
3131 output::info(
3132 "The function may be corrupted. Try re-running 'magellan watch'",
3133 );
3134 std::process::exit(output::EXIT_DATABASE);
3135 }
3136 }
3137 };
3138
3139 let block_id = args.block_id.unwrap_or(0);
3141
3142 let block_exists = cfg.node_indices().any(|n| cfg[n].id == block_id);
3144 if !block_exists {
3145 let valid_blocks: Vec<usize> = cfg.node_indices().map(|n| cfg[n].id).collect();
3146 let msg = format!(
3147 "Block {} not found in function '{}'. Valid blocks: {:?}",
3148 block_id, function_ref, valid_blocks
3149 );
3150 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3151 let error =
3152 output::JsonError::new("BlockNotFound", &msg, output::E_BLOCK_NOT_FOUND);
3153 let wrapper = output::JsonResponse::new(error);
3154 println!("{}", wrapper.to_json());
3155 std::process::exit(output::EXIT_VALIDATION);
3156 } else {
3157 output::error(&msg);
3158 std::process::exit(output::EXIT_VALIDATION);
3159 }
3160 }
3161
3162 let max_depth = if args.max_depth == 100 {
3164 None
3165 } else {
3166 Some(args.max_depth)
3167 };
3168 let impact = find_reachable_from_block(&cfg, block_id, max_depth);
3169
3170 let (forward_impact, backward_impact): (
3172 Option<Vec<CallGraphSymbol>>,
3173 Option<Vec<CallGraphSymbol>>,
3174 ) = if args.use_call_graph {
3175 use crate::analysis::MagellanBridge;
3176 match MagellanBridge::open(&db_path) {
3177 Ok(bridge) => {
3178 let symbol_id = function_name.as_str();
3180 let forward: Option<Vec<CallGraphSymbol>> = bridge
3181 .reachable_symbols(symbol_id)
3182 .map(|symbols| {
3183 symbols
3184 .into_iter()
3185 .map(|s| CallGraphSymbol {
3186 symbol_id: s.symbol_id,
3187 fqn: s.fqn,
3188 file_path: s.file_path,
3189 kind: s.kind,
3190 })
3191 .collect()
3192 })
3193 .ok();
3194 let backward: Option<Vec<CallGraphSymbol>> = bridge
3195 .reverse_reachable_symbols(symbol_id)
3196 .map(|symbols| {
3197 symbols
3198 .into_iter()
3199 .map(|s| CallGraphSymbol {
3200 symbol_id: s.symbol_id,
3201 fqn: s.fqn,
3202 file_path: s.file_path,
3203 kind: s.kind,
3204 })
3205 .collect()
3206 })
3207 .ok();
3208 (forward, backward)
3209 }
3210 Err(e) => {
3211 eprintln!(
3212 "Warning: Could not open Magellan database for call graph analysis: {}",
3213 e
3214 );
3215 eprintln!("Note: --use-call-graph requires a Magellan code graph database");
3216 (None, None)
3217 }
3218 }
3219 } else {
3220 (None, None)
3221 };
3222
3223 match cli.output {
3225 OutputFormat::Human => {
3226 println!("Block Impact Analysis (Blast Zone)");
3227 println!();
3228 println!("Function: {}", function_name);
3229 println!("Source block: {}", impact.source_block_id);
3230 println!();
3231
3232 if let Some(ref forward) = forward_impact {
3234 println!("Inter-Procedural Impact (Call Graph):");
3235 println!(" Forward Impact: {} functions reached", forward.len());
3236 for sym in forward {
3237 println!(" - {}", sym.fqn.as_deref().unwrap_or(&sym.file_path));
3238 }
3239 }
3240 if let Some(ref backward) = backward_impact {
3241 if !backward.is_empty() {
3242 println!(
3243 " Backward Impact: {} functions can reach this",
3244 backward.len()
3245 );
3246 for sym in backward {
3247 println!(" - {}", sym.fqn.as_deref().unwrap_or(&sym.file_path));
3248 }
3249 }
3250 }
3251 println!();
3252
3253 println!("Intra-Procedural Impact (CFG):");
3254 println!(" Reachable blocks: {}", impact.reachable_count);
3255 if impact.reachable_count > 0 {
3256 println!(" Affected blocks: {:?}", impact.reachable_blocks);
3257 } else {
3258 println!(" Affected blocks: (none - block has no downstream impact)");
3259 }
3260 println!(" Max depth reached: {}", impact.max_depth_reached);
3261 println!(
3262 " Contains cycles: {}",
3263 if impact.has_cycles {
3264 "yes (loop detected)"
3265 } else {
3266 "no"
3267 }
3268 );
3269 if let Some(depth) = max_depth {
3270 println!(" Depth limit: {}", depth);
3271 } else {
3272 println!(" Depth limit: unlimited");
3273 }
3274 }
3275 OutputFormat::Json | OutputFormat::Pretty => {
3276 let response = BlockImpactResponse {
3277 function: function_name,
3278 block_id: impact.source_block_id,
3279 reachable_blocks: impact.reachable_blocks,
3280 reachable_count: impact.reachable_count,
3281 max_depth: impact.max_depth_reached,
3282 has_cycles: impact.has_cycles,
3283 forward_impact: forward_impact.clone(),
3284 backward_impact: backward_impact.clone(),
3285 };
3286 let wrapper = output::JsonResponse::new(response);
3287 match cli.output {
3288 OutputFormat::Json => println!("{}", wrapper.to_json()),
3289 OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
3290 _ => unreachable!(),
3291 }
3292 }
3293 }
3294 }
3295
3296 Ok(())
3297 }
3298
3299 pub fn cycles(args: &CyclesArgs, cli: &Cli) -> Result<()> {
3300 use crate::analysis::{CycleInfo, EnhancedCycles, LoopInfo, MagellanBridge};
3301 use crate::cfg::detect_natural_loops;
3302 use crate::cfg::load_cfg_from_db;
3303 use crate::storage::MirageDb;
3304
3305 let db_path = super::resolve_db_path(cli.db.clone())?;
3307
3308 let show_call_graph = args.call_graph
3310 || args.both
3311 || (!args.call_graph && !args.function_loops && !args.both);
3312 let show_function_loops = args.function_loops
3313 || args.both
3314 || (!args.call_graph && !args.function_loops && !args.both);
3315
3316 let mut call_graph_cycles: Vec<CycleInfo> = if show_call_graph {
3318 match MagellanBridge::open(&db_path) {
3319 Ok(bridge) => match bridge.detect_cycles() {
3320 Ok(report) => report.cycles.iter().map(|c| c.into()).collect(),
3321 Err(e) => {
3322 eprintln!("Warning: Failed to detect call graph cycles: {}", e);
3323 vec![]
3324 }
3325 },
3326 Err(e) => {
3327 eprintln!(
3328 "Warning: Could not open Magellan database for call graph cycles: {}",
3329 e
3330 );
3331 eprintln!("Note: Call graph cycles require a Magellan code graph database");
3332 vec![]
3333 }
3334 }
3335 } else {
3336 vec![]
3337 };
3338
3339 call_graph_cycles.retain(|c| match args.cycle_type {
3341 CycleTypeArg::All => true,
3342 CycleTypeArg::InterFunction => c.cycle_type == "MutualRecursion",
3343 CycleTypeArg::SelfLoop => c.cycle_type == "SelfLoop",
3344 });
3345
3346 let mut function_loops_map: std::collections::HashMap<String, Vec<LoopInfo>> =
3348 std::collections::HashMap::new();
3349
3350 if show_function_loops {
3351 let db = match MirageDb::open(&db_path) {
3353 Ok(db) => db,
3354 Err(_e) => {
3355 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3356 let error = output::JsonError::database_not_found(&db_path);
3357 let wrapper = output::JsonResponse::new(error);
3358 println!("{}", wrapper.to_json());
3359 std::process::exit(output::EXIT_DATABASE);
3360 } else {
3361 output::error(&format!("Failed to open database: {}", db_path));
3362 output::info("Hint: Run 'magellan watch' to create the database");
3363 std::process::exit(output::EXIT_DATABASE);
3364 }
3365 }
3366 };
3367
3368 if !db.is_sqlite() {
3371 output::error(
3372 "The 'cycles' command with --function-loops requires SQLite backend.",
3373 );
3374 output::info(
3375 "retired binary backend backend is not yet supported for this feature.",
3376 );
3377 std::process::exit(output::EXIT_USAGE);
3378 }
3379
3380 let mut stmt = match db.conn()?.prepare(
3382 "SELECT name, id FROM graph_entities WHERE kind = 'Symbol' AND json_extract(data, '$.kind') = 'Function'") {
3383 Ok(stmt) => stmt,
3384 Err(e) => {
3385 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3386 let error = output::JsonError::new(
3387 "QueryError",
3388 &format!("Failed to query functions: {}", e),
3389 output::E_DATABASE_NOT_FOUND,
3390 );
3391 let wrapper = output::JsonResponse::new(error);
3392 println!("{}", wrapper.to_json());
3393 std::process::exit(output::EXIT_DATABASE);
3394 } else {
3395 output::error(&format!("Failed to query functions: {}", e));
3396 std::process::exit(output::EXIT_DATABASE);
3397 }
3398 }
3399 };
3400
3401 let rows_result = stmt.query_map([], |row| {
3402 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
3403 });
3404
3405 match rows_result {
3406 Ok(rows) => {
3407 for row in rows {
3408 if let Ok((function_name, function_id)) = row {
3409 if let Ok(cfg) = load_cfg_from_db(&db, function_id) {
3411 let natural_loops = detect_natural_loops(&cfg);
3413
3414 if !natural_loops.is_empty() {
3415 let loop_infos: Vec<LoopInfo> = natural_loops
3416 .iter()
3417 .map(|loop_| {
3418 let nesting_level = loop_.nesting_level(&natural_loops);
3419 let body_blocks: Vec<usize> = loop_
3420 .body
3421 .iter()
3422 .map(|&node| cfg[node].id)
3423 .collect();
3424 LoopInfo {
3425 header: cfg[loop_.header].id,
3426 back_edge_from: cfg[loop_.back_edge.0].id,
3427 body_size: loop_.size(),
3428 nesting_level,
3429 body_blocks,
3430 }
3431 })
3432 .collect();
3433
3434 function_loops_map.insert(function_name, loop_infos);
3435 }
3436 }
3437 }
3438 }
3439 }
3440 Err(e) => {
3441 eprintln!("Warning: Failed to execute query: {}", e);
3442 }
3443 }
3444 }
3445
3446 let total_cycles =
3448 call_graph_cycles.len() + function_loops_map.values().map(|v| v.len()).sum::<usize>();
3449
3450 let enhanced_cycles = EnhancedCycles {
3451 call_graph_cycles,
3452 function_loops: function_loops_map.clone(),
3453 total_cycles,
3454 };
3455
3456 match cli.output {
3458 OutputFormat::Human => {
3459 println!("Cycle Detection Report");
3460 println!();
3461
3462 if show_call_graph {
3463 println!(
3464 "Call Graph Cycles (Inter-procedural): {}",
3465 enhanced_cycles.call_graph_cycles.len()
3466 );
3467 if enhanced_cycles.call_graph_cycles.is_empty() {
3468 println!(" No call graph cycles detected");
3469 } else {
3470 for (i, cycle) in enhanced_cycles.call_graph_cycles.iter().enumerate() {
3471 println!(" Cycle {}:", i + 1);
3472 println!(" Type: {}", cycle.cycle_type);
3473 println!(" Size: {} symbols", cycle.size);
3474 if args.verbose {
3475 println!(" Members:");
3476 for member in &cycle.members {
3477 println!(" - {}", member);
3478 }
3479 }
3480 }
3481 }
3482 println!();
3483 }
3484
3485 if show_function_loops {
3486 println!(
3487 "Function Loops (Intra-procedural): {} functions with loops",
3488 enhanced_cycles.function_loops.len()
3489 );
3490 if enhanced_cycles.function_loops.is_empty() {
3491 println!(" No natural loops detected in any function");
3492 } else {
3493 for (function_name, loops) in &enhanced_cycles.function_loops {
3494 println!(" Function: {} ({} loops)", function_name, loops.len());
3495 if args.verbose {
3496 for (i, loop_info) in loops.iter().enumerate() {
3497 println!(" Loop {}:", i + 1);
3498 println!(" Header: Block {}", loop_info.header);
3499 println!(
3500 " Back edge from: Block {}",
3501 loop_info.back_edge_from
3502 );
3503 println!(" Body size: {} blocks", loop_info.body_size);
3504 println!(" Nesting level: {}", loop_info.nesting_level);
3505 println!(" Body blocks: {:?}", loop_info.body_blocks);
3506 }
3507 }
3508 }
3509 }
3510 println!();
3511 }
3512
3513 println!("Total cycles: {}", total_cycles);
3514 }
3515 OutputFormat::Json | OutputFormat::Pretty => {
3516 let wrapper = output::JsonResponse::new(enhanced_cycles);
3517 match cli.output {
3518 OutputFormat::Json => println!("{}", wrapper.to_json()),
3519 OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
3520 _ => unreachable!(),
3521 }
3522 }
3523 }
3524
3525 Ok(())
3526 }
3527
3528 pub fn slice(args: &SliceArgs, cli: &Cli) -> Result<()> {
3529 use crate::analysis::{MagellanBridge, SliceWrapper};
3530
3531 let db_path = super::resolve_db_path(cli.db.clone())?;
3533
3534 let bridge = match MagellanBridge::open(&db_path) {
3536 Ok(bridge) => bridge,
3537 Err(e) => {
3538 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3539 let error = output::JsonError::new(
3540 "DatabaseError",
3541 &format!("Failed to open Magellan database: {}", e),
3542 output::E_DATABASE_NOT_FOUND,
3543 );
3544 let wrapper = output::JsonResponse::new(error);
3545 println!("{}", wrapper.to_json());
3546 std::process::exit(output::EXIT_DATABASE);
3547 } else {
3548 output::error(&format!("Failed to open Magellan database: {}", e));
3549 output::info("Note: Program slicing requires a Magellan code graph database");
3550 std::process::exit(output::EXIT_DATABASE);
3551 }
3552 }
3553 };
3554
3555 let slice_result: SliceWrapper = match args.direction {
3557 SliceDirectionArg::Backward => bridge.backward_slice(&args.symbol)?,
3558 SliceDirectionArg::Forward => bridge.forward_slice(&args.symbol)?,
3559 };
3560
3561 match cli.output {
3563 OutputFormat::Human => {
3564 println!("Program Slice: {}", slice_result.direction);
3565 println!();
3566
3567 println!("Target:");
3569 println!(
3570 " Symbol: {}",
3571 slice_result.target.fqn.as_deref().unwrap_or(&args.symbol)
3572 );
3573 println!(" Kind: {}", slice_result.target.kind);
3574 println!(" File: {}", slice_result.target.file_path);
3575 println!();
3576
3577 println!("Statistics:");
3579 println!(" Total symbols in slice: {}", slice_result.symbol_count);
3580 println!(
3581 " Data dependencies: {}",
3582 slice_result.statistics.data_dependencies
3583 );
3584 println!(
3585 " Control dependencies: {}",
3586 slice_result.statistics.control_dependencies
3587 );
3588 println!();
3589
3590 if args.verbose {
3592 println!(
3593 "Included symbols ({}):",
3594 slice_result.included_symbols.len()
3595 );
3596 for (i, symbol) in slice_result.included_symbols.iter().enumerate() {
3597 println!(
3598 " {}. {}",
3599 i + 1,
3600 symbol.fqn.as_deref().unwrap_or("<unknown>")
3601 );
3602 println!(" Kind: {}, File: {}", symbol.kind, symbol.file_path);
3603 }
3604 } else {
3605 println!("Use --verbose to see all included symbols");
3606 }
3607 }
3608 OutputFormat::Json | OutputFormat::Pretty => {
3609 let wrapper = output::JsonResponse::new(slice_result);
3610 match cli.output {
3611 OutputFormat::Json => println!("{}", wrapper.to_json()),
3612 OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
3613 _ => unreachable!(),
3614 }
3615 }
3616 }
3617
3618 Ok(())
3619 }
3620
3621 pub fn hotspots(args: &HotspotsArgs, cli: &Cli) -> Result<()> {
3622 use crate::analysis::MagellanBridge;
3623 #[cfg(feature = "sqlite")]
3624 use crate::cfg::{
3625 enumerate_paths_with_context, load_cfg_from_db_with_conn, EnumerationContext,
3626 PathLimits,
3627 };
3628 #[cfg(feature = "sqlite")]
3629 use crate::storage::MirageDb;
3630 use std::collections::HashMap;
3631
3632 let db_path = super::resolve_db_path(cli.db.clone())?;
3633
3634 #[cfg(feature = "sqlite")]
3636 let mut db = match MirageDb::open(&db_path) {
3637 Ok(db) => db,
3638 Err(e) => {
3639 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3640 let error = output::JsonError::new(
3641 "DatabaseError",
3642 &format!("Failed to open database: {}", e),
3643 output::E_DATABASE_NOT_FOUND,
3644 );
3645 let wrapper = output::JsonResponse::new(error);
3646 println!("{}", wrapper.to_json());
3647 std::process::exit(output::EXIT_DATABASE);
3648 } else {
3649 output::error(&format!("Failed to open database: {}", e));
3650 output::info("Hint: Run 'magellan watch' to create the database");
3651 std::process::exit(output::EXIT_DATABASE);
3652 }
3653 }
3654 };
3655
3656 let mut hotspots: Vec<HotspotEntry> = Vec::new();
3657 let mut function_count = 0;
3658
3659 if args.inter_procedural {
3660 match MagellanBridge::open(&db_path) {
3662 Ok(bridge) => {
3663 let path_result = bridge.enumerate_paths(&args.entry, None, 50, args.top * 10);
3665
3666 if let Ok(paths) = path_result {
3667 let mut path_counts: HashMap<String, usize> = HashMap::new();
3669
3670 for path in &paths.paths {
3671 for symbol in &path.symbols {
3672 if let Some(fqn) = &symbol.fqn {
3673 *path_counts.entry(fqn.clone()).or_insert(0) += 1;
3674 }
3675 }
3676 }
3677
3678 let condensed = bridge.condense_call_graph();
3680 if let Ok(condensed) = condensed {
3681 let mut scc_sizes: HashMap<String, f64> = HashMap::new();
3682
3683 for supernode in &condensed.graph.supernodes {
3684 let size = supernode.members.len() as f64;
3685 for member in &supernode.members {
3686 if let Some(fqn) = &member.fqn {
3687 scc_sizes.insert(fqn.clone(), size);
3688 }
3689 }
3690 }
3691
3692 for (fqn, path_count) in &path_counts {
3694 if *path_count >= args.min_paths.unwrap_or(1) {
3695 let dominance = scc_sizes.get(fqn).copied().unwrap_or(1.0);
3696 let risk_score = (*path_count as f64) * 1.0 + dominance * 2.0;
3697
3698 hotspots.push(HotspotEntry {
3699 function: fqn.clone(),
3700 risk_score,
3701 path_count: *path_count,
3702 dominance_factor: dominance,
3703 complexity: 0, file_path: "".to_string(),
3705 });
3706 }
3707 }
3708
3709 function_count = path_counts.len();
3711 }
3712 }
3713 }
3714 Err(_) => {
3715 output::warn(
3716 "Magellan database not available, using intra-procedural analysis",
3717 );
3718 }
3719 }
3720 }
3721
3722 #[cfg(feature = "sqlite")]
3724 if hotspots.is_empty() && db.is_sqlite() {
3725 let conn = db.conn_mut()?;
3727
3728 let query = "SELECT DISTINCT cb.function_id, ge.name, ge.file_path
3729 FROM cfg_blocks cb
3730 JOIN graph_entities ge ON cb.function_id = ge.id";
3731 let mut stmt = conn.prepare(query)?;
3732
3733 let function_rows = stmt.query_map([], |row: &rusqlite::Row| {
3734 Ok((
3735 row.get::<_, i64>(0)?,
3736 row.get::<_, String>(1)?,
3737 row.get::<_, String>(2)?,
3738 ))
3739 })?;
3740
3741 for func_result in function_rows {
3742 if let Ok((func_id, func_name, file_path)) = func_result {
3743 function_count += 1;
3744
3745 if let Ok(cfg) = load_cfg_from_db_with_conn(conn, func_id) {
3747 let ctx = EnumerationContext::new(&cfg);
3748 let limits = PathLimits::quick_analysis();
3749 let paths = enumerate_paths_with_context(&cfg, &limits, &ctx);
3750
3751 let path_count = paths.len();
3752 if path_count < args.min_paths.unwrap_or(1) {
3753 continue;
3754 }
3755
3756 let complexity = cfg.node_count();
3758 let dominance = 1.0; let risk_score = path_count as f64 * 0.5 + complexity as f64 * 0.1;
3760
3761 hotspots.push(HotspotEntry {
3762 function: func_name.clone(),
3763 risk_score,
3764 path_count,
3765 dominance_factor: dominance,
3766 complexity,
3767 file_path,
3768 });
3769 }
3770 }
3771 }
3772 }
3773
3774 hotspots.sort_by(|a, b| b.risk_score.total_cmp(&a.risk_score));
3776
3777 hotspots.truncate(args.top);
3779
3780 let response = HotspotsResponse {
3781 entry_point: args.entry.clone(),
3782 total_functions: function_count,
3783 hotspots: hotspots.clone(),
3784 mode: if args.inter_procedural {
3785 "inter-procedural"
3786 } else {
3787 "intra-procedural"
3788 }
3789 .to_string(),
3790 };
3791
3792 match cli.output {
3793 OutputFormat::Human => {
3794 output::header(&format!(
3795 "Hotspots Analysis (entry: {})",
3796 response.entry_point
3797 ));
3798
3799 if response.total_functions == 0 && response.mode == "intra-procedural" {
3801 output::warn("No functions found. This may be because:");
3802 output::info(" 1. The database hasn't been indexed yet");
3803 output::info(" 2. You need to run: magellan watch --db <path>");
3804 output::info(" 3. Try --inter-procedural for call-graph-based analysis");
3805 println!();
3806 }
3807
3808 output::info(&format!(
3809 "Found {} hotspots out of {} functions",
3810 hotspots.len(),
3811 response.total_functions
3812 ));
3813 println!();
3814
3815 for (i, hotspot) in hotspots.iter().enumerate() {
3816 println!(
3817 "{}. {} (risk: {:.1})",
3818 i + 1,
3819 hotspot.function,
3820 hotspot.risk_score
3821 );
3822 if args.verbose {
3823 println!(" Paths: {}", hotspot.path_count);
3824 println!(" Dominance: {:.1}", hotspot.dominance_factor);
3825 println!(" Complexity: {}", hotspot.complexity);
3826 }
3827 }
3828 }
3829 OutputFormat::Json => {
3830 let wrapper = output::JsonResponse::new(response);
3831 println!("{}", wrapper.to_json());
3832 }
3833 OutputFormat::Pretty => {
3834 let wrapper = output::JsonResponse::new(response);
3835 println!("{}", wrapper.to_pretty_json());
3836 }
3837 }
3838
3839 Ok(())
3840 }
3841
3842 pub fn hotpaths(args: &HotpathsArgs, cli: &Cli) -> Result<()> {
3843 use crate::cfg::{
3844 detect_natural_loops, enumerate_paths, find_entry,
3845 hotpaths::{compute_hot_paths, HotpathsOptions},
3846 PathLimits,
3847 };
3848 use crate::storage::MirageDb;
3849
3850 let db_path = super::resolve_db_path(cli.db.clone())?;
3852
3853 let db = match MirageDb::open(&db_path) {
3855 Ok(db) => db,
3856 Err(_e) => {
3857 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3858 let error = output::JsonError::database_not_found(&db_path);
3859 let wrapper = output::JsonResponse::new(error);
3860 println!("{}", wrapper.to_json());
3861 std::process::exit(output::EXIT_DATABASE);
3862 } else {
3863 output::error(&format!("Failed to open database: {}", db_path));
3864 output::info("Hint: Run 'magellan watch' to create the database");
3865 std::process::exit(output::EXIT_DATABASE);
3866 }
3867 }
3868 };
3869
3870 let function_id = match db.resolve_function_name(&args.function) {
3872 Ok(id) => id,
3873 Err(_e) => {
3874 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3875 let error = output::JsonError::function_not_found(&args.function);
3876 let wrapper = output::JsonResponse::new(error);
3877 println!("{}", wrapper.to_json());
3878 std::process::exit(output::EXIT_DATABASE);
3879 } else {
3880 output::error(&format!(
3881 "Function '{}' not found in database",
3882 args.function
3883 ));
3884 output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
3885 std::process::exit(output::EXIT_DATABASE);
3886 }
3887 }
3888 };
3889
3890 let cfg = match db.load_cfg(function_id) {
3892 Ok(cfg) => cfg,
3893 Err(_e) => {
3894 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3895 let error = output::JsonError::new(
3896 "CfgLoadError",
3897 &format!("Failed to load CFG for function '{}'", args.function),
3898 output::E_CFG_ERROR,
3899 );
3900 let wrapper = output::JsonResponse::new(error);
3901 println!("{}", wrapper.to_json());
3902 std::process::exit(output::EXIT_DATABASE);
3903 } else {
3904 output::error(&format!(
3905 "Failed to load CFG for function '{}'",
3906 args.function
3907 ));
3908 output::info("The function may be corrupted. Try re-running 'magellan watch'");
3909 std::process::exit(output::EXIT_DATABASE);
3910 }
3911 }
3912 };
3913
3914 let entry = match find_entry(&cfg) {
3916 Some(entry) => entry,
3917 None => {
3918 output::error(&format!(
3919 "No entry block found for function '{}'",
3920 args.function
3921 ));
3922 std::process::exit(output::EXIT_DATABASE);
3923 }
3924 };
3925
3926 let natural_loops = detect_natural_loops(&cfg);
3928
3929 let limits = PathLimits::default();
3932 let paths = enumerate_paths(&cfg, &limits);
3933
3934 if paths.is_empty() {
3935 output::info(&format!("No paths found for function '{}'", args.function));
3936 return Ok(());
3937 }
3938
3939 let options = HotpathsOptions {
3941 top_n: args.top,
3942 include_rationale: args.rationale,
3943 };
3944
3945 let mut hot_paths = match compute_hot_paths(&cfg, &paths, entry, &natural_loops, options) {
3946 Ok(hp) => hp,
3947 Err(e) => {
3948 output::error(&format!("Failed to compute hot paths: {}", e));
3949 std::process::exit(output::EXIT_DATABASE);
3950 }
3951 };
3952
3953 if let Some(min_score) = args.min_score {
3955 hot_paths.retain(|hp| hp.hotness_score >= min_score);
3956 }
3957
3958 match cli.output {
3960 OutputFormat::Human => {
3961 print_hotpaths_human(&hot_paths, args.rationale);
3962 }
3963 OutputFormat::Json => {
3964 println!("{}", serde_json::to_string(&hot_paths)?);
3965 }
3966 OutputFormat::Pretty => {
3967 println!("{}", serde_json::to_string_pretty(&hot_paths)?);
3968 }
3969 }
3970
3971 Ok(())
3972 }
3973
3974 pub fn patterns(args: &PatternsArgs, cli: &Cli) -> Result<()> {
3975 use crate::cfg::{detect_if_else_patterns, detect_match_patterns};
3976 use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
3977 use crate::storage::MirageDb;
3978
3979 let db_path = super::resolve_db_path(cli.db.clone())?;
3981
3982 let db = match MirageDb::open(&db_path) {
3984 Ok(db) => db,
3985 Err(_e) => {
3986 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3988 let error = output::JsonError::database_not_found(&db_path);
3989 let wrapper = output::JsonResponse::new(error);
3990 println!("{}", wrapper.to_json());
3991 std::process::exit(output::EXIT_DATABASE);
3992 } else {
3993 output::error(&format!("Failed to open database: {}", db_path));
3994 output::info("Hint: Run 'magellan watch' to create the database");
3995 std::process::exit(output::EXIT_DATABASE);
3996 }
3997 }
3998 };
3999
4000 let function_id =
4002 match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
4003 Ok(id) => id,
4004 Err(_e) => {
4005 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4006 let error = output::JsonError::function_not_found(&args.function);
4007 let wrapper = output::JsonResponse::new(error);
4008 println!("{}", wrapper.to_json());
4009 std::process::exit(output::EXIT_DATABASE);
4010 } else {
4011 output::error(&format!(
4012 "Function '{}' not found in database",
4013 args.function
4014 ));
4015 output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
4016 std::process::exit(output::EXIT_DATABASE);
4017 }
4018 }
4019 };
4020
4021 let cfg = match load_cfg_from_db(&db, function_id) {
4023 Ok(cfg) => cfg,
4024 Err(_e) => {
4025 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4026 let error = output::JsonError::new(
4027 "CgfLoadError",
4028 &format!("Failed to load CFG for function '{}'", args.function),
4029 output::E_CFG_ERROR,
4030 );
4031 let wrapper = output::JsonResponse::new(error);
4032 println!("{}", wrapper.to_json());
4033 std::process::exit(output::EXIT_DATABASE);
4034 } else {
4035 output::error(&format!(
4036 "Failed to load CFG for function '{}'",
4037 args.function
4038 ));
4039 output::info("The function may be corrupted. Try re-running 'magellan watch'");
4040 std::process::exit(output::EXIT_DATABASE);
4041 }
4042 }
4043 };
4044
4045 let show_if_else = !args.r#match; let show_match = !args.if_else; let if_else_patterns = if show_if_else {
4050 detect_if_else_patterns(&cfg)
4051 } else {
4052 vec![]
4053 };
4054
4055 let match_patterns = if show_match {
4056 detect_match_patterns(&cfg)
4057 } else {
4058 vec![]
4059 };
4060
4061 let if_else_infos: Vec<IfElseInfo> = if_else_patterns
4063 .iter()
4064 .map(|p| IfElseInfo {
4065 condition_block: cfg[p.condition].id,
4066 true_branch: cfg[p.true_branch].id,
4067 false_branch: cfg[p.false_branch].id,
4068 merge_point: p.merge_point.map(|n| cfg[n].id),
4069 has_else: p.has_else(),
4070 })
4071 .collect();
4072
4073 let match_infos: Vec<MatchInfo> = match_patterns
4074 .iter()
4075 .map(|p| MatchInfo {
4076 switch_block: cfg[p.switch_node].id,
4077 branch_count: p.branch_count(),
4078 targets: p.targets.iter().map(|n| cfg[*n].id).collect(),
4079 otherwise: cfg[p.otherwise].id,
4080 })
4081 .collect();
4082
4083 match cli.output {
4085 OutputFormat::Human => {
4086 println!("Function: {}", args.function);
4087 println!();
4088
4089 if show_if_else {
4090 println!("If/Else Patterns: {}", if_else_patterns.len());
4091 if if_else_patterns.is_empty() {
4092 output::info("No if/else patterns detected");
4093 } else {
4094 for (i, info) in if_else_infos.iter().enumerate() {
4095 println!(" Pattern {}:", i + 1);
4096 println!(" Condition: Block {}", info.condition_block);
4097 println!(" True branch: Block {}", info.true_branch);
4098 println!(" False branch: Block {}", info.false_branch);
4099 if let Some(merge) = info.merge_point {
4100 println!(" Merge point: Block {}", merge);
4101 println!(" Has else: {}", info.has_else);
4102 } else {
4103 println!(" Merge point: None (no else)");
4104 }
4105 println!();
4106 }
4107 }
4108 println!();
4109 }
4110
4111 if show_match {
4112 println!("Match Patterns: {}", match_patterns.len());
4113 if match_patterns.is_empty() {
4114 output::info("No match patterns detected");
4115 } else {
4116 for (i, info) in match_infos.iter().enumerate() {
4117 println!(" Pattern {}:", i + 1);
4118 println!(" Switch: Block {}", info.switch_block);
4119 println!(" Branch count: {}", info.branch_count);
4120 println!(" Targets: {:?}", info.targets);
4121 println!(" Otherwise: Block {}", info.otherwise);
4122 println!();
4123 }
4124 }
4125 }
4126 }
4127 OutputFormat::Json | OutputFormat::Pretty => {
4128 let response = PatternsResponse {
4129 function: args.function.clone(),
4130 if_else_count: if_else_patterns.len(),
4131 match_count: match_patterns.len(),
4132 if_else_patterns: if_else_infos,
4133 match_patterns: match_infos,
4134 };
4135 let wrapper = output::JsonResponse::new(response);
4136 match cli.output {
4137 OutputFormat::Json => println!("{}", wrapper.to_json()),
4138 OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
4139 _ => unreachable!(),
4140 }
4141 }
4142 }
4143
4144 Ok(())
4145 }
4146
4147 pub fn frontiers(args: &FrontiersArgs, cli: &Cli) -> Result<()> {
4148 use crate::cfg::{compute_dominance_frontiers, DominatorTree};
4149 use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
4150 use crate::storage::MirageDb;
4151
4152 let db_path = super::resolve_db_path(cli.db.clone())?;
4154
4155 let db = match MirageDb::open(&db_path) {
4157 Ok(db) => db,
4158 Err(_e) => {
4159 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4161 let error = output::JsonError::database_not_found(&db_path);
4162 let wrapper = output::JsonResponse::new(error);
4163 println!("{}", wrapper.to_json());
4164 std::process::exit(output::EXIT_DATABASE);
4165 } else {
4166 output::error(&format!("Failed to open database: {}", db_path));
4167 output::info("Hint: Run 'magellan watch' to create the database");
4168 std::process::exit(output::EXIT_DATABASE);
4169 }
4170 }
4171 };
4172
4173 let function_id =
4175 match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
4176 Ok(id) => id,
4177 Err(_e) => {
4178 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4179 let error = output::JsonError::function_not_found(&args.function);
4180 let wrapper = output::JsonResponse::new(error);
4181 println!("{}", wrapper.to_json());
4182 std::process::exit(output::EXIT_DATABASE);
4183 } else {
4184 output::error(&format!(
4185 "Function '{}' not found in database",
4186 args.function
4187 ));
4188 output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
4189 std::process::exit(output::EXIT_DATABASE);
4190 }
4191 }
4192 };
4193
4194 let cfg = match load_cfg_from_db(&db, function_id) {
4196 Ok(cfg) => cfg,
4197 Err(_e) => {
4198 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4199 let error = output::JsonError::new(
4200 "CgfLoadError",
4201 &format!("Failed to load CFG for function '{}'", args.function),
4202 output::E_CFG_ERROR,
4203 );
4204 let wrapper = output::JsonResponse::new(error);
4205 println!("{}", wrapper.to_json());
4206 std::process::exit(output::EXIT_DATABASE);
4207 } else {
4208 output::error(&format!(
4209 "Failed to load CFG for function '{}'",
4210 args.function
4211 ));
4212 output::info("The function may be corrupted. Try re-running 'magellan watch'");
4213 std::process::exit(output::EXIT_DATABASE);
4214 }
4215 }
4216 };
4217
4218 let dom_tree = match DominatorTree::new(&cfg) {
4220 Some(tree) => tree,
4221 None => {
4222 output::error("Could not compute dominator tree (CFG may have no entry blocks)");
4223 std::process::exit(1);
4224 }
4225 };
4226
4227 let frontiers = compute_dominance_frontiers(&cfg, dom_tree);
4229
4230 if args.iterated {
4232 let all_nodes: Vec<petgraph::graph::NodeIndex> = cfg.node_indices().collect();
4234 let iterated_frontier = frontiers.iterated_frontier(&all_nodes);
4235 let iterated_blocks: Vec<usize> =
4236 iterated_frontier.iter().map(|&n| cfg[n].id).collect();
4237
4238 match cli.output {
4239 OutputFormat::Human => {
4240 println!("Function: {}", args.function);
4241 println!("Iterated Dominance Frontier:");
4242 println!("Count: {}", iterated_blocks.len());
4243 println!();
4244 if iterated_blocks.is_empty() {
4245 output::info("No iterated dominance frontier (linear CFG)");
4246 } else {
4247 println!("Blocks in iterated frontier:");
4248 for id in &iterated_blocks {
4249 println!(" - Block {}", id);
4250 }
4251 }
4252 }
4253 OutputFormat::Json | OutputFormat::Pretty => {
4254 let response = IteratedFrontierResponse {
4255 function: args.function.clone(),
4256 iterated_frontier: iterated_blocks,
4257 };
4258 let wrapper = output::JsonResponse::new(response);
4259 match cli.output {
4260 OutputFormat::Json => println!("{}", wrapper.to_json()),
4261 OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
4262 _ => unreachable!(),
4263 }
4264 }
4265 }
4266 } else if let Some(node_id) = args.node {
4267 let target_node = cfg.node_indices().find(|&n| cfg[n].id == node_id);
4269
4270 let target_node = match target_node {
4271 Some(node) => node,
4272 None => {
4273 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4274 let error = output::JsonError::block_not_found(node_id);
4275 let wrapper = output::JsonResponse::new(error);
4276 println!("{}", wrapper.to_json());
4277 std::process::exit(1);
4278 } else {
4279 output::error(&format!("Block {} not found in CFG", node_id));
4280 std::process::exit(1);
4281 }
4282 }
4283 };
4284
4285 let frontier = frontiers.frontier(target_node);
4286 let frontier_blocks: Vec<usize> = frontier.iter().map(|&n| cfg[n].id).collect();
4287
4288 match cli.output {
4289 OutputFormat::Human => {
4290 println!("Function: {}", args.function);
4291 println!("Dominance Frontier for Block {}:", node_id);
4292 println!("Count: {}", frontier_blocks.len());
4293 println!();
4294 if frontier_blocks.is_empty() {
4295 output::info(&format!("Block {} has empty dominance frontier", node_id));
4296 } else {
4297 println!("Frontier blocks:");
4298 for id in &frontier_blocks {
4299 println!(" - Block {}", id);
4300 }
4301 }
4302 }
4303 OutputFormat::Json | OutputFormat::Pretty => {
4304 let response = FrontiersResponse {
4305 function: args.function.clone(),
4306 nodes_with_frontiers: if frontier_blocks.is_empty() { 0 } else { 1 },
4307 frontiers: vec![NodeFrontier {
4308 node: node_id,
4309 frontier_set: frontier_blocks,
4310 }],
4311 };
4312 let wrapper = output::JsonResponse::new(response);
4313 match cli.output {
4314 OutputFormat::Json => println!("{}", wrapper.to_json()),
4315 OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
4316 _ => unreachable!(),
4317 }
4318 }
4319 }
4320 } else {
4321 let nodes_with_frontiers: Vec<NodeFrontier> = frontiers
4323 .nodes_with_frontiers()
4324 .map(|n| {
4325 let frontier = frontiers.frontier(n);
4326 NodeFrontier {
4327 node: cfg[n].id,
4328 frontier_set: frontier.iter().map(|&f| cfg[f].id).collect(),
4329 }
4330 })
4331 .collect();
4332
4333 match cli.output {
4334 OutputFormat::Human => {
4335 println!("Function: {}", args.function);
4336 println!(
4337 "Nodes with non-empty dominance frontiers: {}",
4338 nodes_with_frontiers.len()
4339 );
4340 println!();
4341
4342 if nodes_with_frontiers.is_empty() {
4343 output::info("No dominance frontiers (linear CFG)");
4344 } else {
4345 for node_info in &nodes_with_frontiers {
4346 println!("Block {}:", node_info.node);
4347 println!(" Frontier: {:?}", node_info.frontier_set);
4348 println!();
4349 }
4350 }
4351 }
4352 OutputFormat::Json | OutputFormat::Pretty => {
4353 let response = FrontiersResponse {
4354 function: args.function.clone(),
4355 nodes_with_frontiers: nodes_with_frontiers.len(),
4356 frontiers: nodes_with_frontiers,
4357 };
4358 let wrapper = output::JsonResponse::new(response);
4359 match cli.output {
4360 OutputFormat::Json => println!("{}", wrapper.to_json()),
4361 OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
4362 _ => unreachable!(),
4363 }
4364 }
4365 }
4366 }
4367
4368 Ok(())
4369 }
4370
4371 pub fn diff(args: &DiffArgs, cli: &Cli) -> Result<()> {
4372 use crate::cfg::diff::compute_cfg_diff;
4373 use crate::storage::MirageDb;
4374
4375 let db_path = super::resolve_db_path(cli.db.clone())?;
4377
4378 let db = match MirageDb::open(&db_path) {
4380 Ok(db) => db,
4381 Err(_e) => {
4382 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4383 let error = output::JsonError::database_not_found(&db_path);
4384 let wrapper = output::JsonResponse::new(error);
4385 println!("{}", wrapper.to_json());
4386 std::process::exit(output::EXIT_DATABASE);
4387 } else {
4388 output::error(&format!("Failed to open database: {}", db_path));
4389 output::info("Hint: Run 'magellan watch' to create the database");
4390 std::process::exit(output::EXIT_DATABASE);
4391 }
4392 }
4393 };
4394
4395 let function_id = match db.resolve_function_name(&args.function) {
4397 Ok(id) => id,
4398 Err(e) => {
4399 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4400 let error = output::JsonError::new("Database", &e.to_string(), "E001");
4401 let wrapper = output::JsonResponse::new(error);
4402 println!("{}", wrapper.to_json());
4403 std::process::exit(output::EXIT_DATABASE);
4404 } else {
4405 output::error(&format!("Failed to resolve function: {}", e));
4406 std::process::exit(output::EXIT_DATABASE);
4407 }
4408 }
4409 };
4410
4411 let diff = match compute_cfg_diff(db.storage(), function_id, &args.before, &args.after) {
4413 Ok(diff) => diff,
4414 Err(e) => {
4415 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4416 let error = output::JsonError::new("Database", &e.to_string(), "E001");
4417 let wrapper = output::JsonResponse::new(error);
4418 println!("{}", wrapper.to_json());
4419 std::process::exit(output::EXIT_DATABASE);
4420 } else {
4421 return Err(e);
4422 }
4423 }
4424 };
4425
4426 match cli.output {
4428 OutputFormat::Human => print_diff_human(&diff, args.show_edges, args.verbose),
4429 OutputFormat::Json => {
4430 let wrapper = output::JsonResponse::new(diff);
4431 println!("{}", wrapper.to_json());
4432 }
4433 OutputFormat::Pretty => {
4434 let wrapper = output::JsonResponse::new(diff);
4435 println!("{}", wrapper.to_pretty_json());
4436 }
4437 }
4438
4439 Ok(())
4440 }
4441
4442 fn print_diff_human(diff: &crate::cfg::diff::CfgDiff, show_edges: bool, verbose: bool) {
4443 use crate::output::{info, success, warn};
4444
4445 info(&format!("CFG Diff: {}", diff.function_name));
4446 println!(" Before: {}", diff.before_snapshot);
4447 println!(" After: {}", diff.after_snapshot);
4448
4449 let similarity_pct = diff.structural_similarity * 100.0;
4451 if similarity_pct >= 90.0 {
4452 success(&format!(" Similarity: {:.1}%", similarity_pct));
4453 } else if similarity_pct >= 70.0 {
4454 println!(" Similarity: {:.1}%", similarity_pct);
4455 } else {
4456 warn(&format!(" Similarity: {:.1}%", similarity_pct));
4457 }
4458
4459 if !diff.added_blocks.is_empty() {
4460 println!();
4461 info(&format!("Added blocks ({}):", diff.added_blocks.len()));
4462 for block in &diff.added_blocks {
4463 println!(
4464 " + Block {}: {} @ {}",
4465 block.block_id, block.kind, block.source_location
4466 );
4467 }
4468 }
4469
4470 if !diff.deleted_blocks.is_empty() {
4471 println!();
4472 info(&format!("Deleted blocks ({}):", diff.deleted_blocks.len()));
4473 for block in &diff.deleted_blocks {
4474 println!(
4475 " - Block {}: {} @ {}",
4476 block.block_id, block.kind, block.source_location
4477 );
4478 }
4479 }
4480
4481 if !diff.modified_blocks.is_empty() && verbose {
4482 println!();
4483 info(&format!(
4484 "Modified blocks ({}):",
4485 diff.modified_blocks.len()
4486 ));
4487 for change in &diff.modified_blocks {
4488 match &change.change_type {
4489 crate::cfg::diff::ChangeType::TerminatorChanged { before, after } => {
4490 println!(" ~ Block {}: {} -> {}", change.block_id, before, after);
4491 }
4492 crate::cfg::diff::ChangeType::SourceLocationChanged => {
4493 println!(" ~ Block {}: location changed", change.block_id);
4494 }
4495 crate::cfg::diff::ChangeType::BothChanged => {
4496 println!(
4497 " ~ Block {}: terminator and location changed",
4498 change.block_id
4499 );
4500 }
4501 crate::cfg::diff::ChangeType::EdgesChanged => {
4502 println!(" ~ Block {}: edges changed", change.block_id);
4503 }
4504 }
4505 }
4506 }
4507
4508 if show_edges {
4509 if !diff.added_edges.is_empty() {
4510 println!();
4511 info(&format!("Added edges ({}):", diff.added_edges.len()));
4512 for edge in &diff.added_edges {
4513 println!(
4514 " + {} -> {} ({})",
4515 edge.from_block, edge.to_block, edge.edge_type
4516 );
4517 }
4518 }
4519 if !diff.deleted_edges.is_empty() {
4520 println!();
4521 info(&format!("Deleted edges ({}):", diff.deleted_edges.len()));
4522 for edge in &diff.deleted_edges {
4523 println!(
4524 " - {} -> {} ({})",
4525 edge.from_block, edge.to_block, edge.edge_type
4526 );
4527 }
4528 }
4529 }
4530
4531 if diff.added_blocks.is_empty()
4533 && diff.deleted_blocks.is_empty()
4534 && diff.modified_blocks.is_empty()
4535 && diff.added_edges.is_empty()
4536 && diff.deleted_edges.is_empty()
4537 {
4538 println!();
4539 success("No changes detected");
4540 }
4541 }
4542
4543 pub fn icfg(args: &IcfgArgs, cli: &Cli) -> Result<()> {
4544 use crate::cfg::icfg::{build_icfg, to_dot, IcfgJson, IcfgOptions};
4545 use crate::output::error;
4546 use crate::output::{EXIT_DATABASE, EXIT_NOT_FOUND};
4547 use crate::storage::MirageDb;
4548
4549 let db_path = super::resolve_db_path(cli.db.clone())?;
4550
4551 let db = match MirageDb::open(&db_path) {
4553 Ok(db) => db,
4554 Err(e) => {
4555 error(&format!("Failed to open database: {}", e));
4556 std::process::exit(EXIT_DATABASE);
4557 }
4558 };
4559
4560 let function_id = match db.resolve_function_name(&args.entry) {
4562 Ok(id) => id,
4563 Err(_) => {
4564 error(&format!("Function not found: {}", args.entry));
4565 std::process::exit(EXIT_NOT_FOUND);
4566 }
4567 };
4568
4569 let options = IcfgOptions {
4571 max_depth: args.depth,
4572 include_return_edges: args.return_edges,
4573 };
4574
4575 let icfg = match build_icfg(db.storage(), db.backend(), function_id, options) {
4577 Ok(icfg) => icfg,
4578 Err(e) => {
4579 error(&format!("Failed to build ICFG: {}", e));
4580 std::process::exit(EXIT_DATABASE);
4581 }
4582 };
4583
4584 let format = args.format.unwrap_or(match cli.output {
4586 OutputFormat::Human => IcfgFormat::Human,
4587 _ => IcfgFormat::Dot,
4588 });
4589
4590 match format {
4591 IcfgFormat::Dot => {
4592 println!("{}", to_dot(&icfg));
4593 }
4594 IcfgFormat::Json => {
4595 let json_repr = IcfgJson::from_icfg(&icfg);
4596 println!("{}", serde_json::to_string_pretty(&json_repr)?);
4597 }
4598 IcfgFormat::Human => {
4599 print_icfg_human(&icfg);
4600 }
4601 }
4602
4603 Ok(())
4604 }
4605
4606 fn print_icfg_human(icfg: &crate::cfg::icfg::Icfg) {
4607 use std::collections::HashSet;
4608 println!("Inter-Procedural CFG");
4609 println!(" Entry function: {}", icfg.entry_function);
4610
4611 let mut functions = HashSet::new();
4613 for node in icfg.graph.node_indices() {
4614 functions.insert(icfg.graph[node].function_id);
4615 }
4616 println!(" Functions: {}", functions.len());
4617 println!(" Nodes: {}", icfg.graph.node_count());
4618 println!(" Edges: {}", icfg.graph.edge_count());
4619
4620 let mut call_count = 0;
4622 let mut return_count = 0;
4623 let mut intra_count = 0;
4624
4625 for edge in icfg.graph.edge_indices() {
4626 match &icfg.graph[edge] {
4627 crate::cfg::icfg::IcfgEdge::Call { .. } => call_count += 1,
4628 crate::cfg::icfg::IcfgEdge::Return { .. } => return_count += 1,
4629 crate::cfg::icfg::IcfgEdge::IntraProcedural { .. } => intra_count += 1,
4630 }
4631 }
4632
4633 println!(" Edges by type:");
4634 println!(" Call: {}", call_count);
4635 println!(" Return: {}", return_count);
4636 println!(" Intra-procedural: {}", intra_count);
4637 }
4638
4639 pub fn migrate(args: &MigrateArgs, cli: &Cli) -> Result<()> {
4640 use crate::storage::BackendFormat as StorageBackendFormat;
4641
4642 let db_path = std::path::Path::new(&args.db);
4643
4644 if !db_path.exists() {
4646 return Err(anyhow::anyhow!("Database not found: {}", args.db));
4647 }
4648
4649 let actual_format = StorageBackendFormat::detect(db_path)
4651 .map_err(|e| anyhow::anyhow!("Backend detection failed: {}", e))?;
4652
4653 let actual_format_cli = match actual_format {
4655 StorageBackendFormat::SQLite => BackendFormat::Sqlite,
4656 StorageBackendFormat::Geometric => BackendFormat::Geometric,
4657 StorageBackendFormat::Unknown => {
4658 return Err(anyhow::anyhow!(
4659 "Cannot detect backend format: unknown format"
4660 ));
4661 }
4662 };
4663
4664 if args.from != actual_format_cli {
4666 return Err(anyhow::anyhow!(
4667 "Source backend mismatch: expected {}, found {:?}",
4668 args.from,
4669 actual_format
4670 ));
4671 }
4672
4673 if args.from == args.to {
4675 return Err(anyhow::anyhow!(
4676 "Source and target backends must be different"
4677 ));
4678 }
4679
4680 if args.dry_run {
4682 match cli.output {
4683 OutputFormat::Human => {
4684 println!("Dry run: would migrate {} -> {}", args.from, args.to);
4685 println!("Database: {}", args.db);
4686 }
4687 OutputFormat::Json | OutputFormat::Pretty => {
4688 let output = serde_json::json!({
4689 "dry_run": true,
4690 "from": args.from.to_string(),
4691 "to": args.to.to_string(),
4692 "database": args.db,
4693 });
4694 match cli.output {
4695 OutputFormat::Json => println!("{}", serde_json::to_string(&output)?),
4696 OutputFormat::Pretty => {
4697 println!("{}", serde_json::to_string_pretty(&output)?)
4698 }
4699 _ => unreachable!(),
4700 }
4701 }
4702 }
4703 return Ok(());
4704 }
4705
4706 if args.backup {
4708 let timestamp = std::time::SystemTime::now()
4709 .duration_since(std::time::UNIX_EPOCH)
4710 .map(|d| d.as_secs())
4711 .unwrap_or_else(|_| {
4712 std::time::SystemTime::now()
4714 .elapsed()
4715 .map(|d| d.as_secs())
4716 .unwrap_or(0)
4717 });
4718 let backup_path = format!("{}.backup.{}", args.db, timestamp);
4719 std::fs::copy(&args.db, &backup_path)
4720 .map_err(|e| anyhow::anyhow!("Failed to create backup: {}", e))?;
4721 eprintln!("Backup created: {}", backup_path);
4722 }
4723
4724 Err(anyhow::anyhow!(
4725 "Backend migration is not supported by this Mirage build; use SQLite .magellan/<project>.db databases"
4726 ))
4727 }
4728 pub fn coverage(args: &CoverageArgs, cli: &Cli) -> Result<()> {
4729 use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
4730 use crate::storage::MirageDb;
4731
4732 let db_path = super::resolve_db_path(cli.db.clone())?;
4734
4735 let db = match MirageDb::open(&db_path) {
4737 Ok(db) => db,
4738 Err(_e) => {
4739 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4740 let error = output::JsonError::database_not_found(&db_path);
4741 let wrapper = output::JsonResponse::new(error);
4742 println!("{}", wrapper.to_json());
4743 std::process::exit(output::EXIT_DATABASE);
4744 } else {
4745 output::error(&format!("Failed to open database: {}", db_path));
4746 output::info("Hint: Run 'magellan watch' to create the database");
4747 std::process::exit(output::EXIT_DATABASE);
4748 }
4749 }
4750 };
4751
4752 let function_id =
4754 match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
4755 Ok(id) => id,
4756 Err(_e) => {
4757 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4758 let error = output::JsonError::function_not_found(&args.function);
4759 let wrapper = output::JsonResponse::new(error);
4760 println!("{}", wrapper.to_json());
4761 std::process::exit(output::EXIT_DATABASE);
4762 } else {
4763 output::error(&format!(
4764 "Function '{}' not found in database",
4765 args.function
4766 ));
4767 output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
4768 std::process::exit(output::EXIT_DATABASE);
4769 }
4770 }
4771 };
4772
4773 let cfg = match load_cfg_from_db(&db, function_id) {
4775 Ok(cfg) => cfg,
4776 Err(_e) => {
4777 if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4778 let error = output::JsonError::new(
4779 "CgfLoadError",
4780 &format!("Failed to load CFG for function '{}'", args.function),
4781 output::E_CFG_ERROR,
4782 );
4783 let wrapper = output::JsonResponse::new(error);
4784 println!("{}", wrapper.to_json());
4785 std::process::exit(output::EXIT_DATABASE);
4786 } else {
4787 output::error(&format!(
4788 "Failed to load CFG for function '{}'",
4789 args.function
4790 ));
4791 output::info("The function may be corrupted. Try re-running 'magellan watch'");
4792 std::process::exit(output::EXIT_DATABASE);
4793 }
4794 }
4795 };
4796
4797 let coverage_rows: Vec<(usize, String, i64)> =
4799 db.conn().ok().map_or_else(Vec::new, |conn| {
4800 let sql = "SELECT bb.id, bb.kind, COALESCE(bc.hit_count, 0) \
4801 FROM cfg_blocks bb \
4802 LEFT JOIN cfg_block_coverage bc ON bb.id = bc.block_id \
4803 WHERE bb.function_id = ?1 \
4804 ORDER BY bb.byte_start";
4805 let mut stmt = match conn.prepare(sql) {
4806 Ok(s) => s,
4807 Err(_) => return Vec::new(),
4808 };
4809 let rows = stmt.query_map([function_id], |row| {
4810 Ok((
4811 row.get::<_, i64>(0)? as usize,
4812 row.get::<_, String>(1)?,
4813 row.get::<_, i64>(2)?,
4814 ))
4815 });
4816 match rows {
4817 Ok(iter) => iter.filter_map(|r| r.ok()).collect(),
4818 Err(_) => Vec::new(),
4819 }
4820 });
4821
4822 let db_id_to_graph_id: std::collections::HashMap<i64, usize> = cfg
4824 .node_indices()
4825 .filter_map(|idx| {
4826 cfg.node_weight(idx)
4827 .and_then(|b| b.db_id.map(|db_id| (db_id, b.id)))
4828 })
4829 .collect();
4830
4831 match cli.output {
4832 OutputFormat::Human => {
4833 println!("Coverage for function '{}'", args.function);
4834 println!("{}", "=".repeat(60));
4835 if coverage_rows.is_empty() {
4836 println!("No coverage data available.");
4837 println!("Hint: Run tests with 'cargo test' to generate coverage.");
4838 } else {
4839 for (db_id, kind, hits) in &coverage_rows {
4840 let graph_id = db_id_to_graph_id
4841 .get(&(*db_id as i64))
4842 .map(|id| id.to_string())
4843 .unwrap_or_else(|| "?".to_string());
4844 println!(
4845 " Block {:>3} (graph #{}, kind={:>8}): {:>6} hits",
4846 db_id, graph_id, kind, hits
4847 );
4848 }
4849 }
4850 }
4851 OutputFormat::Json | OutputFormat::Pretty => {
4852 #[derive(serde::Serialize)]
4853 struct CoverageEntry {
4854 block_id: usize,
4855 graph_id: Option<usize>,
4856 kind: String,
4857 hit_count: i64,
4858 }
4859 let entries: Vec<CoverageEntry> = coverage_rows
4860 .iter()
4861 .map(|(db_id, kind, hits)| CoverageEntry {
4862 block_id: *db_id,
4863 graph_id: db_id_to_graph_id.get(&(*db_id as i64)).copied(),
4864 kind: kind.to_string(),
4865 hit_count: *hits,
4866 })
4867 .collect();
4868 let response = output::JsonResponse::new(serde_json::json!({
4869 "function": args.function,
4870 "coverage": entries,
4871 }));
4872 match cli.output {
4873 OutputFormat::Json => println!("{}", response.to_json()),
4874 OutputFormat::Pretty => println!("{}", response.to_pretty_json()),
4875 _ => unreachable!(),
4876 }
4877 }
4878 }
4879
4880 Ok(())
4881 }
4882}
4883
4884fn print_hotpaths_human(hot_paths: &[crate::cfg::hotpaths::HotPath], show_rationale: bool) {
4890 use crate::output;
4891
4892 output::header(&format!("Hot Paths (top {})", hot_paths.len()));
4893
4894 if hot_paths.is_empty() {
4895 output::info("No hot paths found");
4896 return;
4897 }
4898
4899 for (i, hp) in hot_paths.iter().enumerate() {
4900 println!(
4901 "\n{}. Path {} - Score: {:.2}",
4902 i + 1,
4903 hp.path_id,
4904 hp.hotness_score
4905 );
4906
4907 if show_rationale && !hp.rationale.is_empty() {
4908 println!(" Rationale:");
4909 for r in &hp.rationale {
4910 println!(" - {}", r);
4911 }
4912 }
4913
4914 println!(" Blocks: {} blocks", hp.blocks.len());
4915 for (j, block) in hp.blocks.iter().enumerate() {
4916 if j < 5 || j == hp.blocks.len() - 1 {
4917 print!(" {}", block);
4918 if j == 4 && hp.blocks.len() > 6 {
4919 println!(" ... (+{} more)", hp.blocks.len() - 6);
4920 break;
4921 } else {
4922 println!();
4923 }
4924 }
4925 }
4926 }
4927}
4928
4929#[cfg(test)]
4934mod tests {
4935 use super::*;
4936
4937 fn clear_env() {
4939 std::env::remove_var("MIRAGE_DB");
4940 }
4941
4942 #[test]
4943 fn test_resolve_db_path_no_source() {
4944 clear_env();
4945 let result = resolve_db_path(None);
4948 match result {
4951 Ok(path) => {
4952 assert!(
4954 path.ends_with(".db") || path.ends_with(".db"),
4955 "Auto-discovered DB should have .db or .db extension"
4956 );
4957 }
4958 Err(_) => {
4959 }
4961 }
4962 }
4963
4964 #[test]
4965 fn test_resolve_db_path_with_cli_arg() {
4966 clear_env();
4967 let result = resolve_db_path(Some("/custom/path.db".to_string())).unwrap();
4969 assert_eq!(result, "/custom/path.db");
4970 }
4971
4972 #[test]
4973 fn test_resolve_db_path_with_env_var() {
4974 clear_env();
4975 std::env::set_var("MIRAGE_DB", "/env/path.db");
4977 let result = resolve_db_path(None).unwrap();
4978 assert_eq!(result, "/env/path.db");
4979 std::env::remove_var("MIRAGE_DB");
4980 }
4981
4982 #[test]
4983 fn test_resolve_db_path_cli_overrides_env() {
4984 clear_env();
4985 std::env::set_var("MIRAGE_DB", "/env/path.db");
4987 let result = resolve_db_path(Some("/cli/path.db".to_string())).unwrap();
4988 assert_eq!(result, "/cli/path.db");
4989 std::env::remove_var("MIRAGE_DB");
4990 }
4991}
4992
4993#[cfg(test)]
4998mod cfg_tests {
4999 use super::*;
5000 use crate::cfg::{export_dot, export_json};
5001
5002 #[test]
5004 fn test_cfg_dot_format() {
5005 let cfg = cmds::create_test_cfg();
5006 let dot = export_dot(&cfg);
5007
5008 assert!(
5010 dot.contains("digraph CFG"),
5011 "DOT output should contain 'digraph CFG'"
5012 );
5013 assert!(
5014 dot.contains("rankdir=TB"),
5015 "DOT output should contain rankdir attribute"
5016 );
5017 assert!(
5018 dot.contains("node [shape=box"),
5019 "DOT output should contain node shape attribute"
5020 );
5021 assert!(
5022 dot.contains("}"),
5023 "DOT output should end with closing brace"
5024 );
5025
5026 assert!(dot.contains("->"), "DOT output should contain edge arrows");
5028 }
5029
5030 #[test]
5032 fn test_cfg_json_format() {
5033 let cfg = cmds::create_test_cfg();
5034 let function_name = "test_function";
5035 let export = export_json(&cfg, function_name, None);
5036
5037 assert_eq!(
5039 export.function_name, function_name,
5040 "JSON export should include function name"
5041 );
5042
5043 assert!(
5045 export.entry.is_some(),
5046 "JSON export should have an entry block"
5047 );
5048 assert!(
5049 !export.exits.is_empty(),
5050 "JSON export should have exit blocks"
5051 );
5052 assert!(!export.blocks.is_empty(), "JSON export should have blocks");
5053 assert!(!export.edges.is_empty(), "JSON export should have edges");
5054
5055 let json_str = serde_json::to_string(&export);
5057 assert!(
5058 json_str.is_ok(),
5059 "JSON export should be serializable to JSON"
5060 );
5061
5062 let json = json_str.unwrap();
5064 assert!(
5065 json.contains(function_name),
5066 "JSON output should contain function name"
5067 );
5068 assert!(
5069 json.contains("\"entry\""),
5070 "JSON output should contain entry field"
5071 );
5072 assert!(
5073 json.contains("\"exits\""),
5074 "JSON output should contain exits field"
5075 );
5076 assert!(
5077 json.contains("\"blocks\""),
5078 "JSON output should contain blocks field"
5079 );
5080 assert!(
5081 json.contains("\"edges\""),
5082 "JSON output should contain edges field"
5083 );
5084 }
5085
5086 #[test]
5088 fn test_cfg_function_name_in_export() {
5089 let cfg = cmds::create_test_cfg();
5090
5091 let test_names = vec!["my_function", "TestFunc", "module::submodule::function"];
5093
5094 for name in test_names {
5095 let export = export_json(&cfg, name, None);
5096 assert_eq!(
5097 export.function_name, name,
5098 "Function name should be preserved in export"
5099 );
5100 }
5101 }
5102
5103 #[test]
5105 fn test_cfg_format_fallback() {
5106 let cli_human = Cli {
5108 db: None,
5109 output: OutputFormat::Human,
5110 command: Some(Commands::Cfg(CfgArgs {
5111 function: "test".to_string(),
5112 file: None,
5113 format: None,
5114 })),
5115 detect_backend: false,
5116 };
5117
5118 let cfg_args = match &cli_human.command {
5119 Some(Commands::Cfg(args)) => args,
5120 _ => panic!("Expected Cfg command"),
5121 };
5122
5123 let resolved_format = cfg_args.format.unwrap_or(match cli_human.output {
5125 OutputFormat::Human => CfgFormat::Human,
5126 OutputFormat::Json => CfgFormat::Json,
5127 OutputFormat::Pretty => CfgFormat::Json,
5128 });
5129
5130 assert_eq!(
5131 resolved_format,
5132 CfgFormat::Human,
5133 "Should fall back to Human format"
5134 );
5135
5136 let cli_json = Cli {
5138 db: None,
5139 output: OutputFormat::Json,
5140 command: Some(Commands::Cfg(CfgArgs {
5141 function: "test".to_string(),
5142 file: None,
5143 format: None,
5144 })),
5145 detect_backend: false,
5146 };
5147
5148 let cfg_args_json = match &cli_json.command {
5149 Some(Commands::Cfg(args)) => args,
5150 _ => panic!("Expected Cfg command"),
5151 };
5152
5153 let resolved_format_json = cfg_args_json.format.unwrap_or(match cli_json.output {
5154 OutputFormat::Human => CfgFormat::Human,
5155 OutputFormat::Json => CfgFormat::Json,
5156 OutputFormat::Pretty => CfgFormat::Json,
5157 });
5158
5159 assert_eq!(
5160 resolved_format_json,
5161 CfgFormat::Json,
5162 "Should fall back to Json format"
5163 );
5164 }
5165
5166 #[test]
5168 fn test_cfg_json_response_wrapper() {
5169 use crate::output::JsonResponse;
5170
5171 let cfg = cmds::create_test_cfg();
5172 let export = export_json(&cfg, "wrapped_function", None);
5173 let response = JsonResponse::new(export);
5174
5175 assert_eq!(response.schema_version, "1.0.1");
5177 assert_eq!(response.tool, "mirage");
5178 assert!(!response.execution_id.is_empty());
5179 assert!(!response.timestamp.is_empty());
5180
5181 let json = response.to_json();
5183 assert!(json.contains("\"schema_version\""));
5184 assert!(json.contains("\"execution_id\""));
5185 assert!(json.contains("\"tool\":\"mirage\""));
5186 assert!(json.contains("\"data\""));
5187 assert!(json.contains("wrapped_function"));
5188 }
5189
5190 #[test]
5192 fn test_cfg_dot_block_info() {
5193 let cfg = cmds::create_test_cfg();
5194 let dot = export_dot(&cfg);
5195
5196 assert!(
5198 dot.contains("lightgreen"),
5199 "DOT should mark entry block with green"
5200 );
5201
5202 assert!(
5204 dot.contains("lightcoral"),
5205 "DOT should mark exit blocks with coral"
5206 );
5207
5208 assert!(dot.contains("Block"), "DOT should contain block labels");
5210 }
5211
5212 #[test]
5214 fn test_cfg_dot_edge_info() {
5215 let cfg = cmds::create_test_cfg();
5216 let dot = export_dot(&cfg);
5217
5218 assert!(
5220 dot.contains("color=green"),
5221 "DOT should show true branch edges in green"
5222 );
5223 assert!(
5224 dot.contains("color=red"),
5225 "DOT should show false branch edges in red"
5226 );
5227 }
5228}
5229
5230#[cfg(test)]
5235mod status_tests {
5236 use crate::storage::{create_schema, MirageDb};
5237 use rusqlite::{params, Connection};
5238
5239 fn create_test_db() -> anyhow::Result<(tempfile::NamedTempFile, MirageDb)> {
5241 use crate::storage::{
5242 REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
5243 };
5244
5245 let file = tempfile::NamedTempFile::new()?;
5246 let mut conn = Connection::open(file.path())?;
5247
5248 conn.execute(
5250 "CREATE TABLE magellan_meta (
5251 id INTEGER PRIMARY KEY CHECK (id = 1),
5252 magellan_schema_version INTEGER NOT NULL,
5253 sqlitegraph_schema_version INTEGER NOT NULL,
5254 created_at INTEGER NOT NULL
5255 )",
5256 [],
5257 )?;
5258
5259 conn.execute(
5260 "CREATE TABLE graph_entities (
5261 id INTEGER PRIMARY KEY AUTOINCREMENT,
5262 kind TEXT NOT NULL,
5263 name TEXT NOT NULL,
5264 file_path TEXT,
5265 data TEXT NOT NULL
5266 )",
5267 [],
5268 )?;
5269
5270 conn.execute(
5271 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
5272 VALUES (1, ?, ?, ?)",
5273 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
5274 )?;
5275
5276 create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION)?;
5278
5279 conn.execute(
5281 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
5282 params!("function", "test_func", "test.rs", "{}"),
5283 )?;
5284 let function_id: i64 = conn.last_insert_rowid();
5285
5286 conn.execute(
5288 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end, start_line, start_col, end_line, end_col)
5289 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
5290 params!(function_id, "entry", "goto", 0, 10, 1, 0, 1, 10),
5291 )?;
5292 conn.execute(
5293 "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end, start_line, start_col, end_line, end_col)
5294 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
5295 params!(function_id, "return", "return", 10, 20, 2, 0, 2, 10),
5296 )?;
5297
5298 conn.execute(
5303 "INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
5304 VALUES (?, ?, ?, ?, ?, ?, ?)",
5305 params!("test_path", function_id, "normal", 1, 2, 2, 0),
5306 )?;
5307
5308 conn.execute(
5310 "INSERT INTO cfg_dominators (block_id, dominator_id, is_strict) VALUES (?, ?, ?)",
5311 params!(1, 1, false),
5312 )?;
5313
5314 let db = MirageDb::open(file.path())?;
5315 Ok((file, db))
5316 }
5317
5318 #[test]
5320 #[cfg(feature = "backend-sqlite")]
5321 #[allow(deprecated)]
5322 fn test_status_returns_correct_statistics() {
5323 let (_file, db) = create_test_db().unwrap();
5324 let status = db.status().unwrap();
5325
5326 assert_eq!(status.cfg_blocks, 2, "Should have 2 cfg_blocks");
5327 assert_eq!(
5329 status.cfg_edges, 0,
5330 "cfg_edges count should be 0 (managed by Magellan)"
5331 );
5332 assert_eq!(status.cfg_paths, 1, "Should have 1 cfg_path");
5333 assert_eq!(status.cfg_dominators, 1, "Should have 1 cfg_dominator");
5334 assert_eq!(
5335 status.mirage_schema_version, 1,
5336 "Schema version should be 1"
5337 );
5338 assert_eq!(
5339 status.magellan_schema_version, 7,
5340 "Magellan version should be 7"
5341 );
5342 }
5343
5344 #[test]
5346 #[cfg(feature = "backend-sqlite")]
5347 #[allow(deprecated)]
5348 fn test_status_human_output_format() {
5349 let (_file, db) = create_test_db().unwrap();
5350 let status = db.status().unwrap();
5351
5352 assert!(status.cfg_blocks >= 0, "cfg_blocks should be non-negative");
5354 assert!(status.cfg_edges >= 0, "cfg_edges should be non-negative");
5355 assert!(status.cfg_paths >= 0, "cfg_paths should be non-negative");
5356 assert!(
5357 status.cfg_dominators >= 0,
5358 "cfg_dominators should be non-negative"
5359 );
5360 assert!(
5361 status.mirage_schema_version > 0,
5362 "mirage_schema_version should be positive"
5363 );
5364 assert!(
5365 status.magellan_schema_version > 0,
5366 "magellan_schema_version should be positive"
5367 );
5368 }
5369
5370 #[test]
5372 #[cfg(feature = "backend-sqlite")]
5373 fn test_status_json_output_format() {
5374 use crate::output::JsonResponse;
5375
5376 let (_file, db) = create_test_db().unwrap();
5377 let status = db.status().unwrap();
5378 let response = JsonResponse::new(status);
5379
5380 assert_eq!(response.schema_version, "1.0.1");
5382 assert_eq!(response.tool, "mirage");
5383 assert!(!response.execution_id.is_empty());
5384 assert!(!response.timestamp.is_empty());
5385
5386 let json = response.to_json();
5388 assert!(json.contains("\"schema_version\":\"1.0.1\""));
5389 assert!(json.contains("\"tool\":\"mirage\""));
5390 assert!(json.contains("\"execution_id\""));
5391 assert!(json.contains("\"timestamp\""));
5392 assert!(json.contains("\"data\""));
5393 assert!(json.contains("\"cfg_blocks\""));
5394 assert!(json.contains("\"cfg_edges\""));
5395 assert!(json.contains("\"cfg_paths\""));
5396 assert!(json.contains("\"cfg_dominators\""));
5397 assert!(json.contains("\"mirage_schema_version\""));
5398 assert!(json.contains("\"magellan_schema_version\""));
5399 }
5400
5401 #[test]
5403 #[cfg(feature = "backend-sqlite")]
5404 fn test_status_pretty_json_output_format() {
5405 use crate::output::JsonResponse;
5406
5407 let (_file, db) = create_test_db().unwrap();
5408 let status = db.status().unwrap();
5409 let response = JsonResponse::new(status);
5410
5411 let pretty_json = response.to_pretty_json();
5412
5413 assert!(
5415 pretty_json.contains("\n"),
5416 "Pretty JSON should contain newlines"
5417 );
5418 assert!(
5419 pretty_json.contains(" "),
5420 "Pretty JSON should contain indentation"
5421 );
5422
5423 let parsed: serde_json::Value =
5425 serde_json::from_str(&pretty_json).expect("Pretty JSON should be valid");
5426 assert!(parsed.is_object(), "Parsed JSON should be an object");
5427 assert_eq!(parsed["schema_version"], "1.0.1");
5428 assert_eq!(parsed["tool"], "mirage");
5429 assert!(parsed["data"].is_object(), "data field should be an object");
5430 }
5431
5432 #[test]
5434 fn test_status_database_open_error() {
5435 use crate::storage::MirageDb;
5436
5437 let result = MirageDb::open("/nonexistent/path/to/database.db");
5439
5440 match result {
5442 Ok(_) => panic!("Should fail to open non-existent database"),
5443 Err(e) => {
5444 let err_msg = e.to_string();
5445 assert!(
5446 err_msg.contains("Database not found") || err_msg.contains("not found"),
5447 "Error message should mention database not found: {}",
5448 err_msg
5449 );
5450 }
5451 }
5452 }
5453
5454 #[test]
5456 #[cfg(feature = "backend-sqlite")]
5457 #[allow(deprecated)]
5458 fn test_status_empty_database_returns_zeros() {
5459 use crate::storage::{
5460 REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
5461 };
5462
5463 let file = tempfile::NamedTempFile::new().unwrap();
5464 let mut conn = Connection::open(file.path()).unwrap();
5465
5466 conn.execute(
5468 "CREATE TABLE magellan_meta (
5469 id INTEGER PRIMARY KEY CHECK (id = 1),
5470 magellan_schema_version INTEGER NOT NULL,
5471 sqlitegraph_schema_version INTEGER NOT NULL,
5472 created_at INTEGER NOT NULL
5473 )",
5474 [],
5475 )
5476 .unwrap();
5477
5478 conn.execute(
5479 "CREATE TABLE graph_entities (
5480 id INTEGER PRIMARY KEY AUTOINCREMENT,
5481 kind TEXT NOT NULL,
5482 name TEXT NOT NULL,
5483 file_path TEXT,
5484 data TEXT NOT NULL
5485 )",
5486 [],
5487 )
5488 .unwrap();
5489
5490 conn.execute(
5491 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
5492 VALUES (1, ?, ?, ?)",
5493 params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
5494 ).unwrap();
5495
5496 create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
5497
5498 let db = MirageDb::open(file.path()).unwrap();
5499 let status = db.status().unwrap();
5500
5501 assert_eq!(
5502 status.cfg_blocks, 0,
5503 "Empty database should have 0 cfg_blocks"
5504 );
5505 assert_eq!(
5507 status.cfg_edges, 0,
5508 "cfg_edges count should be 0 (table managed by Magellan)"
5509 );
5510 assert_eq!(
5511 status.cfg_paths, 0,
5512 "Empty database should have 0 cfg_paths"
5513 );
5514 assert_eq!(
5515 status.cfg_dominators, 0,
5516 "Empty database should have 0 cfg_dominators"
5517 );
5518 }
5519}
5520
5521#[cfg(test)]
5526mod paths_tests {
5527 use super::*;
5528 use crate::cfg::{enumerate_paths, PathKind, PathLimits};
5529
5530 #[test]
5532 fn test_paths_enumeration_basic() {
5533 let cfg = cmds::create_test_cfg();
5534 let limits = PathLimits::default();
5535 let paths = enumerate_paths(&cfg, &limits);
5536
5537 assert!(!paths.is_empty(), "Should find at least one path");
5539 assert_eq!(paths.len(), 2, "Test CFG should have exactly 2 paths");
5540
5541 let normal_count = paths.iter().filter(|p| p.kind == PathKind::Normal).count();
5543 assert_eq!(normal_count, 2, "Both paths should be Normal");
5544 }
5545
5546 #[test]
5548 fn test_paths_show_errors_filter() {
5549 let cfg = cmds::create_test_cfg();
5550 let limits = PathLimits::default();
5551 let mut paths = enumerate_paths(&cfg, &limits);
5552
5553 paths.retain(|p| p.kind == PathKind::Error);
5555
5556 assert_eq!(paths.len(), 0, "Test CFG should have no error paths");
5558
5559 for path in &paths {
5561 assert_eq!(
5562 path.kind,
5563 PathKind::Error,
5564 "Filtered paths should all be Error kind"
5565 );
5566 }
5567 }
5568
5569 #[test]
5571 fn test_paths_max_length_limit() {
5572 let cfg = cmds::create_test_cfg();
5573
5574 let limits = PathLimits::default().with_max_length(1);
5576 let paths = enumerate_paths(&cfg, &limits);
5577
5578 for path in &paths {
5580 assert!(path.len() <= 1, "Path length should be <= max_length limit");
5581 }
5582
5583 let unlimited_paths = enumerate_paths(&cfg, &PathLimits::default());
5585 assert!(
5586 paths.len() <= unlimited_paths.len(),
5587 "Limited enumeration should produce <= paths than unlimited"
5588 );
5589 }
5590
5591 #[test]
5593 fn test_paths_args_function_extraction() {
5594 let args = PathsArgs {
5595 function: "test_function".to_string(),
5596 file: None,
5597 show_errors: false,
5598 max_length: None,
5599 with_blocks: false,
5600 incremental: false,
5601 since: None,
5602 by_coverage: false,
5603 };
5604
5605 assert_eq!(args.function, "test_function");
5606 assert!(!args.show_errors);
5607 assert!(args.max_length.is_none());
5608 assert!(!args.with_blocks);
5609 }
5610
5611 #[test]
5613 fn test_paths_args_with_flags() {
5614 let args = PathsArgs {
5615 function: "my_func".to_string(),
5616 file: None,
5617 show_errors: true,
5618 max_length: Some(10),
5619 with_blocks: true,
5620 incremental: false,
5621 since: None,
5622 by_coverage: false,
5623 };
5624
5625 assert_eq!(args.function, "my_func");
5626 assert!(args.show_errors, "show_errors flag should be true");
5627 assert_eq!(args.max_length, Some(10), "max_length should be Some(10)");
5628 assert!(args.with_blocks, "with_blocks flag should be true");
5629 }
5630
5631 #[test]
5633 fn test_path_summary_from_path() {
5634 use crate::cfg::Path;
5635
5636 let path = Path::new(vec![0, 1, 2], PathKind::Normal);
5637 let summary = PathSummary::from(path);
5638
5639 assert!(!summary.path_id.is_empty(), "path_id should not be empty");
5640 assert_eq!(summary.kind, "Normal", "kind should match PathKind");
5641 assert_eq!(summary.length, 3, "length should match path length");
5642
5643 assert_eq!(summary.blocks.len(), 3, "should have 3 blocks");
5645 assert_eq!(summary.blocks[0].block_id, 0, "first block_id should be 0");
5646 assert_eq!(summary.blocks[1].block_id, 1, "second block_id should be 1");
5647 assert_eq!(summary.blocks[2].block_id, 2, "third block_id should be 2");
5648 assert_eq!(
5649 summary.blocks[0].terminator, "Unknown",
5650 "terminator should be Unknown placeholder"
5651 );
5652
5653 assert!(summary.summary.is_none(), "summary should be None");
5655 assert!(
5656 summary.source_range.is_none(),
5657 "source_range should be None"
5658 );
5659 }
5660
5661 #[test]
5663 fn test_path_summary_different_kinds() {
5664 use crate::cfg::Path;
5665
5666 let kinds = vec![
5667 (PathKind::Normal, "Normal"),
5668 (PathKind::Error, "Error"),
5669 (PathKind::Degenerate, "Degenerate"),
5670 (PathKind::Unreachable, "Unreachable"),
5671 ];
5672
5673 for (kind, expected_str) in kinds {
5674 let path = Path::new(vec![0, 1], kind);
5675 let summary = PathSummary::from(path);
5676 assert_eq!(
5677 summary.kind, expected_str,
5678 "PathKind::{:?} should serialize to {}",
5679 kind, expected_str
5680 );
5681 }
5682 }
5683
5684 #[test]
5686 fn test_paths_response_multiple_paths() {
5687 use crate::cfg::Path;
5688
5689 let paths = vec![
5690 Path::new(vec![0, 1], PathKind::Normal),
5691 Path::new(vec![0, 2], PathKind::Normal),
5692 Path::new(vec![0, 1, 3], PathKind::Error),
5693 ];
5694
5695 let summaries: Vec<PathSummary> = paths.into_iter().map(PathSummary::from).collect();
5696
5697 assert_eq!(summaries.len(), 3, "Should have 3 summaries");
5698
5699 let error_summaries = summaries.iter().filter(|s| s.kind == "Error").count();
5701 assert_eq!(error_summaries, 1, "Should have 1 error path");
5702 }
5703
5704 #[test]
5706 fn test_paths_response_metadata() {
5707 let response = PathsResponse {
5708 function: "test_func".to_string(),
5709 total_paths: 5,
5710 error_paths: 2,
5711 paths: vec![],
5712 };
5713
5714 assert_eq!(response.function, "test_func");
5715 assert_eq!(response.total_paths, 5);
5716 assert_eq!(response.error_paths, 2);
5717 assert!(response.paths.is_empty());
5718 }
5719
5720 #[test]
5722 fn test_paths_integration_with_test_cfg() {
5723 let cfg = cmds::create_test_cfg();
5724 let limits = PathLimits::default();
5725 let paths = enumerate_paths(&cfg, &limits);
5726
5727 assert!(!paths.is_empty(), "Test CFG should produce paths");
5729
5730 for path in &paths {
5732 assert_eq!(path.blocks[0], 0, "All paths should start at entry block 0");
5733 assert_eq!(path.entry, 0, "Path entry should be block 0");
5734 }
5735
5736 for path in &paths {
5738 assert!(
5739 path.exit == 2 || path.exit == 3,
5740 "Path exit should be either block 2 or 3 (the return blocks)"
5741 );
5742 }
5743 }
5744
5745 #[test]
5747 fn test_paths_args_with_blocks_flag() {
5748 let args_with = PathsArgs {
5749 function: "test".to_string(),
5750 file: None,
5751 show_errors: false,
5752 max_length: None,
5753 with_blocks: true,
5754 incremental: false,
5755 since: None,
5756 by_coverage: false,
5757 };
5758
5759 let args_without = PathsArgs {
5760 function: "test".to_string(),
5761 file: None,
5762 show_errors: false,
5763 max_length: None,
5764 with_blocks: false,
5765 incremental: false,
5766 since: None,
5767 by_coverage: false,
5768 };
5769
5770 assert!(args_with.with_blocks, "with_blocks should be true");
5771 assert!(!args_without.with_blocks, "with_blocks should be false");
5772 }
5773
5774 #[test]
5776 fn test_path_summary_from_with_cfg() {
5777 use crate::cfg::{
5778 BasicBlock, BlockKind, EdgeType, Path, PathKind, SourceLocation, Terminator,
5779 };
5780 use petgraph::graph::DiGraph;
5781 use std::path::PathBuf;
5782
5783 let mut g = DiGraph::new();
5785
5786 let loc0 = SourceLocation {
5787 file_path: PathBuf::from("test.rs"),
5788 byte_start: 0,
5789 byte_end: 10,
5790 start_line: 1,
5791 start_column: 1,
5792 end_line: 1,
5793 end_column: 10,
5794 };
5795
5796 let loc1 = SourceLocation {
5797 file_path: PathBuf::from("test.rs"),
5798 byte_start: 11,
5799 byte_end: 20,
5800 start_line: 2,
5801 start_column: 1,
5802 end_line: 2,
5803 end_column: 10,
5804 };
5805
5806 let loc2 = SourceLocation {
5807 file_path: PathBuf::from("test.rs"),
5808 byte_start: 21,
5809 byte_end: 30,
5810 start_line: 3,
5811 start_column: 1,
5812 end_line: 3,
5813 end_column: 10,
5814 };
5815
5816 let b0 = g.add_node(BasicBlock {
5817 id: 0,
5818 db_id: None,
5819 kind: BlockKind::Entry,
5820 statements: vec!["let x = 1".to_string()],
5821 terminator: Terminator::Goto { target: 1 },
5822 source_location: Some(loc0),
5823 coord_x: 0,
5824 coord_y: 0,
5825 coord_z: 0,
5826 });
5827
5828 let b1 = g.add_node(BasicBlock {
5829 id: 1,
5830 db_id: None,
5831 kind: BlockKind::Normal,
5832 statements: vec!["if x > 0".to_string()],
5833 terminator: Terminator::SwitchInt {
5834 targets: vec![2],
5835 otherwise: 2,
5836 },
5837 source_location: Some(loc1),
5838 coord_x: 0,
5839 coord_y: 0,
5840 coord_z: 0,
5841 });
5842
5843 let b2 = g.add_node(BasicBlock {
5844 id: 2,
5845 db_id: None,
5846 kind: BlockKind::Exit,
5847 statements: vec!["return true".to_string()],
5848 terminator: Terminator::Return,
5849 source_location: Some(loc2),
5850 coord_x: 0,
5851 coord_y: 0,
5852 coord_z: 0,
5853 });
5854
5855 g.add_edge(b0, b1, EdgeType::Fallthrough);
5856 g.add_edge(b1, b2, EdgeType::TrueBranch);
5857
5858 let path = Path::new(vec![0, 1, 2], PathKind::Normal);
5860 let summary = PathSummary::from_with_cfg(path, &g);
5861
5862 assert_eq!(summary.blocks[0].terminator, "Goto { target: 1 }");
5864 assert!(summary.blocks[1].terminator.contains("SwitchInt"));
5865 assert_eq!(summary.blocks[2].terminator, "Return");
5866
5867 assert!(
5869 summary.source_range.is_some(),
5870 "source_range should be Some"
5871 );
5872 let sr = summary.source_range.as_ref().unwrap();
5873 assert_eq!(sr.file_path, "test.rs");
5874 assert_eq!(sr.start_line, 1);
5875 assert_eq!(sr.end_line, 3);
5876 }
5877
5878 #[test]
5880 fn test_path_summary_from_with_cfg_no_source_locations() {
5881 use crate::cfg::{Path, PathKind};
5882
5883 let cfg = cmds::create_test_cfg();
5885 let path = Path::new(vec![0, 1, 2], PathKind::Normal);
5886 let summary = PathSummary::from_with_cfg(path, &cfg);
5887
5888 assert!(summary.blocks[0].terminator.contains("Goto"));
5890 assert!(summary.blocks[1].terminator.contains("SwitchInt"));
5891 assert_eq!(summary.blocks[2].terminator, "Return");
5892
5893 assert!(
5895 summary.source_range.is_none(),
5896 "source_range should be None when CFG has no locations"
5897 );
5898 }
5899
5900 #[test]
5906 fn test_paths_cache_miss_first_call() {
5907 use crate::cfg::get_or_enumerate_paths;
5908 use crate::storage::create_schema;
5909 use rusqlite::Connection;
5910
5911 let mut conn = Connection::open_in_memory().unwrap();
5913
5914 conn.execute(
5916 "CREATE TABLE magellan_meta (
5917 id INTEGER PRIMARY KEY CHECK (id = 1),
5918 magellan_schema_version INTEGER NOT NULL,
5919 sqlitegraph_schema_version INTEGER NOT NULL,
5920 created_at INTEGER NOT NULL
5921 )",
5922 [],
5923 )
5924 .unwrap();
5925
5926 conn.execute(
5927 "CREATE TABLE graph_entities (
5928 id INTEGER PRIMARY KEY AUTOINCREMENT,
5929 kind TEXT NOT NULL,
5930 name TEXT NOT NULL,
5931 file_path TEXT,
5932 data TEXT NOT NULL
5933 )",
5934 [],
5935 )
5936 .unwrap();
5937
5938 conn.execute(
5939 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
5940 VALUES (1, 4, 3, 0)",
5941 [],
5942 ).unwrap();
5943
5944 create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
5946
5947 let cfg = cmds::create_test_cfg();
5949 let limits = PathLimits::default();
5950 let test_function_id: i64 = 1; conn.execute(
5953 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
5954 rusqlite::params!("function", "test_func", "test.rs", "{}"),
5955 )
5956 .unwrap();
5957
5958 conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
5960 let test_function_hash: &str = "test_cfg";
5961
5962 let paths1 = get_or_enumerate_paths(
5964 &cfg,
5965 test_function_id,
5966 test_function_hash,
5967 &limits,
5968 &mut conn,
5969 )
5970 .unwrap();
5971
5972 assert!(
5974 !paths1.is_empty(),
5975 "First call should enumerate and return paths"
5976 );
5977 assert_eq!(paths1.len(), 2, "Test CFG should have 2 paths");
5978
5979 let path_count: i64 = conn
5981 .query_row(
5982 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
5983 rusqlite::params![test_function_id],
5984 |row| row.get(0),
5985 )
5986 .unwrap();
5987
5988 assert_eq!(
5989 path_count, 2,
5990 "Paths should be stored in database after first call"
5991 );
5992
5993 }
5995
5996 #[test]
5998 fn test_paths_cache_hit_second_call() {
5999 use crate::cfg::get_or_enumerate_paths;
6000 use crate::storage::create_schema;
6001 use rusqlite::Connection;
6002
6003 let mut conn = Connection::open_in_memory().unwrap();
6005
6006 conn.execute(
6008 "CREATE TABLE magellan_meta (
6009 id INTEGER PRIMARY KEY CHECK (id = 1),
6010 magellan_schema_version INTEGER NOT NULL,
6011 sqlitegraph_schema_version INTEGER NOT NULL,
6012 created_at INTEGER NOT NULL
6013 )",
6014 [],
6015 )
6016 .unwrap();
6017
6018 conn.execute(
6019 "CREATE TABLE graph_entities (
6020 id INTEGER PRIMARY KEY AUTOINCREMENT,
6021 kind TEXT NOT NULL,
6022 name TEXT NOT NULL,
6023 file_path TEXT,
6024 data TEXT NOT NULL
6025 )",
6026 [],
6027 )
6028 .unwrap();
6029
6030 conn.execute(
6031 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
6032 VALUES (1, 4, 3, 0)",
6033 [],
6034 ).unwrap();
6035
6036 create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
6038 conn.execute(
6040 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
6041 rusqlite::params!("function", "test_func", "test.rs", "{}"),
6042 )
6043 .unwrap();
6044
6045 conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
6047
6048 let cfg = cmds::create_test_cfg();
6050 let limits = PathLimits::default();
6051 let test_function_id: i64 = 1; let test_function_hash: &str = "test_cfg";
6053
6054 let paths1 = get_or_enumerate_paths(
6056 &cfg,
6057 test_function_id,
6058 test_function_hash,
6059 &limits,
6060 &mut conn,
6061 )
6062 .unwrap();
6063
6064 let path_count: i64 = conn
6066 .query_row(
6067 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
6068 rusqlite::params![test_function_id],
6069 |row| row.get(0),
6070 )
6071 .unwrap();
6072 assert_eq!(path_count, 2, "Should have 2 paths stored after first call");
6073
6074 let paths2 = get_or_enumerate_paths(
6076 &cfg,
6077 test_function_id,
6078 test_function_hash,
6079 &limits,
6080 &mut conn,
6081 )
6082 .unwrap();
6083
6084 assert_eq!(
6086 paths2.len(),
6087 paths1.len(),
6088 "Cache hit should return same number of paths"
6089 );
6090
6091 let mut path_ids1: Vec<_> = paths1.iter().map(|p| &p.path_id).collect();
6093 let mut path_ids2: Vec<_> = paths2.iter().map(|p| &p.path_id).collect();
6094 path_ids1.sort();
6095 path_ids2.sort();
6096
6097 assert_eq!(
6098 path_ids1, path_ids2,
6099 "Cache hit should return paths with same IDs"
6100 );
6101
6102 for (p1, p2) in paths1.iter().zip(paths2.iter()) {
6104 assert_eq!(p1.path_id, p2.path_id, "Path IDs should match on cache hit");
6105 assert_eq!(p1.kind, p2.kind, "Path kinds should match on cache hit");
6106 assert_eq!(
6107 p1.blocks, p2.blocks,
6108 "Path blocks should match on cache hit"
6109 );
6110 }
6111 }
6112
6113 #[test]
6115 fn test_paths_cache_invalidation_on_hash_change() {
6116 use crate::cfg::get_or_enumerate_paths;
6117 use crate::storage::create_schema;
6118 use rusqlite::Connection;
6119
6120 let mut conn = Connection::open_in_memory().unwrap();
6122
6123 conn.execute(
6125 "CREATE TABLE magellan_meta (
6126 id INTEGER PRIMARY KEY CHECK (id = 1),
6127 magellan_schema_version INTEGER NOT NULL,
6128 sqlitegraph_schema_version INTEGER NOT NULL,
6129 created_at INTEGER NOT NULL
6130 )",
6131 [],
6132 )
6133 .unwrap();
6134
6135 conn.execute(
6136 "CREATE TABLE graph_entities (
6137 id INTEGER PRIMARY KEY AUTOINCREMENT,
6138 kind TEXT NOT NULL,
6139 name TEXT NOT NULL,
6140 file_path TEXT,
6141 data TEXT NOT NULL
6142 )",
6143 [],
6144 )
6145 .unwrap();
6146
6147 conn.execute(
6148 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
6149 VALUES (1, 4, 3, 0)",
6150 [],
6151 ).unwrap();
6152
6153 create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
6155 conn.execute(
6157 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
6158 rusqlite::params!("function", "test_func", "test.rs", "{}"),
6159 )
6160 .unwrap();
6161
6162 conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
6164
6165 let cfg = cmds::create_test_cfg();
6167 let limits = PathLimits::default();
6168 let test_function_id: i64 = 1; let test_function_hash_v1: &str = "test_cfg_v1";
6170 let test_function_hash_v3: &str = "test_cfg_v3";
6171
6172 let paths1 = get_or_enumerate_paths(
6174 &cfg,
6175 test_function_id,
6176 test_function_hash_v1,
6177 &limits,
6178 &mut conn,
6179 )
6180 .unwrap();
6181
6182 let path_count_v1: i64 = conn
6184 .query_row(
6185 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
6186 rusqlite::params![test_function_id],
6187 |row| row.get(0),
6188 )
6189 .unwrap();
6190
6191 assert_eq!(path_count_v1, 2, "Should have 2 paths after first call");
6192
6193 let paths2 = get_or_enumerate_paths(
6197 &cfg,
6198 test_function_id,
6199 test_function_hash_v3,
6200 &limits,
6201 &mut conn,
6202 )
6203 .unwrap();
6204
6205 assert!(!paths2.is_empty(), "Should re-enumerate");
6207 assert_eq!(
6208 paths2.len(),
6209 paths1.len(),
6210 "Re-enumeration should produce same paths"
6211 );
6212
6213 let path_count: i64 = conn
6215 .query_row(
6216 "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
6217 rusqlite::params![test_function_id],
6218 |row| row.get(0),
6219 )
6220 .unwrap();
6221
6222 assert_eq!(path_count, 2, "Should have 2 paths after re-enumeration");
6223 }
6224}
6225
6226#[cfg(test)]
6231mod unreachable_tests {
6232 use super::*;
6233 use crate::cfg::reachability::find_unreachable;
6234 use crate::cfg::{BasicBlock, BlockKind, Cfg, EdgeType, Terminator};
6235 use petgraph::graph::DiGraph;
6236
6237 fn create_cfg_with_unreachable() -> Cfg {
6239 let mut g = DiGraph::new();
6240
6241 let b0 = g.add_node(BasicBlock {
6243 id: 0,
6244 db_id: None,
6245 kind: BlockKind::Entry,
6246 statements: vec!["let x = 1".to_string()],
6247 terminator: Terminator::Goto { target: 1 },
6248 source_location: None,
6249 coord_x: 0,
6250 coord_y: 0,
6251 coord_z: 0,
6252 });
6253
6254 let b1 = g.add_node(BasicBlock {
6256 id: 1,
6257 db_id: None,
6258 kind: BlockKind::Normal,
6259 statements: vec!["if x > 0".to_string()],
6260 terminator: Terminator::SwitchInt {
6261 targets: vec![2],
6262 otherwise: 3,
6263 },
6264 source_location: None,
6265 coord_x: 0,
6266 coord_y: 0,
6267 coord_z: 0,
6268 });
6269
6270 let b2 = g.add_node(BasicBlock {
6272 id: 2,
6273 db_id: None,
6274 kind: BlockKind::Exit,
6275 statements: vec!["return true".to_string()],
6276 terminator: Terminator::Return,
6277 source_location: None,
6278 coord_x: 0,
6279 coord_y: 0,
6280 coord_z: 0,
6281 });
6282
6283 let b3 = g.add_node(BasicBlock {
6285 id: 3,
6286 db_id: None,
6287 kind: BlockKind::Exit,
6288 statements: vec!["return false".to_string()],
6289 terminator: Terminator::Return,
6290 source_location: None,
6291 coord_x: 0,
6292 coord_y: 0,
6293 coord_z: 0,
6294 });
6295
6296 let _b4 = g.add_node(BasicBlock {
6298 id: 4,
6299 db_id: None,
6300 kind: BlockKind::Exit,
6301 statements: vec!["unreachable code".to_string()],
6302 terminator: Terminator::Unreachable,
6303 source_location: None,
6304 coord_x: 0,
6305 coord_y: 0,
6306 coord_z: 0,
6307 });
6308
6309 g.add_edge(b0, b1, EdgeType::Fallthrough);
6310 g.add_edge(b1, b2, EdgeType::TrueBranch);
6311 g.add_edge(b1, b3, EdgeType::FalseBranch);
6312
6313 g
6314 }
6315
6316 #[test]
6318 fn test_unreachable_detects_dead_code() {
6319 let cfg = create_cfg_with_unreachable();
6320 let unreachable_indices = find_unreachable(&cfg);
6321
6322 assert_eq!(
6324 unreachable_indices.len(),
6325 1,
6326 "Should find exactly 1 unreachable block"
6327 );
6328
6329 let block_id = cfg.node_weight(unreachable_indices[0]).unwrap().id;
6331 assert_eq!(block_id, 4, "Unreachable block should be block 4");
6332 }
6333
6334 #[test]
6336 fn test_unreachable_response_serialization() {
6337 use crate::output::JsonResponse;
6338
6339 let response = UnreachableResponse {
6340 uncalled_functions: None,
6341 function: "test_func".to_string(),
6342 total_functions: 1,
6343 functions_with_unreachable: 1,
6344 unreachable_count: 1,
6345 blocks: vec![UnreachableBlock {
6346 block_id: 4,
6347 kind: "Exit".to_string(),
6348 statements: vec!["unreachable code".to_string()],
6349 terminator: "Unreachable".to_string(),
6350 incoming_edges: vec![],
6351 }],
6352 };
6353
6354 let wrapper = JsonResponse::new(response);
6355 let json = wrapper.to_json();
6356
6357 assert!(json.contains("\"function\":\"test_func\""));
6358 assert!(json.contains("\"unreachable_count\":1"));
6359 assert!(json.contains("\"block_id\":4"));
6360 assert!(json.contains("\"kind\":\"Exit\""));
6361 }
6362
6363 #[test]
6365 fn test_unreachable_empty_response() {
6366 use crate::output::JsonResponse;
6367
6368 let response = UnreachableResponse {
6369 uncalled_functions: None,
6370 function: "test_func".to_string(),
6371 total_functions: 1,
6372 functions_with_unreachable: 0,
6373 unreachable_count: 0,
6374 blocks: vec![],
6375 };
6376
6377 let wrapper = JsonResponse::new(response);
6378 let json = wrapper.to_json();
6379
6380 assert!(json.contains("\"unreachable_count\":0"));
6381 assert!(json.contains("\"functions_with_unreachable\":0"));
6382 }
6383
6384 #[test]
6386 fn test_unreachable_block_fields() {
6387 let block = UnreachableBlock {
6388 block_id: 5,
6389 kind: "Normal".to_string(),
6390 statements: vec!["stmt1".to_string(), "stmt2".to_string()],
6391 terminator: "Return".to_string(),
6392 incoming_edges: vec![],
6393 };
6394
6395 assert_eq!(block.block_id, 5);
6396 assert_eq!(block.kind, "Normal");
6397 assert_eq!(block.statements.len(), 2);
6398 assert_eq!(block.terminator, "Return");
6399 }
6400
6401 #[test]
6403 fn test_unreachable_args_flags() {
6404 let args_with = UnreachableArgs {
6405 include_uncalled: false,
6406 within_functions: true,
6407 show_branches: true,
6408 };
6409
6410 let args_without = UnreachableArgs {
6411 include_uncalled: false,
6412 within_functions: false,
6413 show_branches: false,
6414 };
6415
6416 assert!(args_with.within_functions);
6417 assert!(args_with.show_branches);
6418 assert!(!args_without.within_functions);
6419 assert!(!args_without.show_branches);
6420 }
6421
6422 #[test]
6424 fn test_test_cfg_fully_reachable() {
6425 let cfg = cmds::create_test_cfg();
6426 let unreachable_indices = find_unreachable(&cfg);
6427
6428 assert_eq!(
6430 unreachable_indices.len(),
6431 0,
6432 "Test CFG should have no unreachable blocks"
6433 );
6434 }
6435
6436 #[test]
6438 fn test_unreachable_show_branches_with_edges() {
6439 use crate::cfg::reachability::find_unreachable;
6440 use petgraph::visit::EdgeRef;
6441
6442 let mut g = DiGraph::new();
6445
6446 let b0 = g.add_node(BasicBlock {
6447 id: 0,
6448 db_id: None,
6449 kind: BlockKind::Entry,
6450 statements: vec!["let x = 1".to_string()],
6451 terminator: Terminator::Goto { target: 1 },
6452 source_location: None,
6453 coord_x: 0,
6454 coord_y: 0,
6455 coord_z: 0,
6456 });
6457
6458 let b1 = g.add_node(BasicBlock {
6459 id: 1,
6460 db_id: None,
6461 kind: BlockKind::Normal,
6462 statements: vec!["if x > 0".to_string()],
6463 terminator: Terminator::SwitchInt {
6464 targets: vec![2],
6465 otherwise: 3,
6466 },
6467 source_location: None,
6468 coord_x: 0,
6469 coord_y: 0,
6470 coord_z: 0,
6471 });
6472
6473 let b2 = g.add_node(BasicBlock {
6474 id: 2,
6475 db_id: None,
6476 kind: BlockKind::Exit,
6477 statements: vec!["return true".to_string()],
6478 terminator: Terminator::Return,
6479 source_location: None,
6480 coord_x: 0,
6481 coord_y: 0,
6482 coord_z: 0,
6483 });
6484
6485 let b3 = g.add_node(BasicBlock {
6487 id: 3,
6488 db_id: None,
6489 kind: BlockKind::Normal,
6490 statements: vec!["unreachable branch".to_string()],
6491 terminator: Terminator::Goto { target: 4 },
6492 source_location: None,
6493 coord_x: 0,
6494 coord_y: 0,
6495 coord_z: 0,
6496 });
6497
6498 let b4 = g.add_node(BasicBlock {
6499 id: 4,
6500 db_id: None,
6501 kind: BlockKind::Exit,
6502 statements: vec!["unreachable code".to_string()],
6503 terminator: Terminator::Unreachable,
6504 source_location: None,
6505 coord_x: 0,
6506 coord_y: 0,
6507 coord_z: 0,
6508 });
6509
6510 g.add_edge(b0, b1, EdgeType::Fallthrough);
6512 g.add_edge(b1, b2, EdgeType::TrueBranch);
6513 g.add_edge(b3, b4, EdgeType::Fallthrough);
6515
6516 let unreachable_indices = find_unreachable(&g);
6518 let blocks: Vec<UnreachableBlock> = unreachable_indices
6519 .iter()
6520 .map(|&idx| {
6521 let block = &g[idx];
6522 let kind_str = format!("{:?}", block.kind);
6523 let terminator_str = format!("{:?}", block.terminator);
6524
6525 let incoming_edges: Vec<IncomingEdge> = g
6527 .edge_references()
6528 .filter(|edge| edge.target() == idx)
6529 .map(|edge| {
6530 let source_block = &g[edge.source()];
6531 let edge_type = g.edge_weight(edge.id()).unwrap();
6532 IncomingEdge {
6533 from_block: source_block.id,
6534 edge_type: format!("{:?}", edge_type),
6535 }
6536 })
6537 .collect();
6538
6539 UnreachableBlock {
6540 block_id: block.id,
6541 kind: kind_str,
6542 statements: block.statements.clone(),
6543 terminator: terminator_str,
6544 incoming_edges,
6545 }
6546 })
6547 .collect();
6548
6549 assert_eq!(blocks.len(), 2);
6551
6552 let block3 = blocks.iter().find(|b| b.block_id == 3).unwrap();
6554 assert_eq!(block3.incoming_edges.len(), 0);
6555
6556 let block4 = blocks.iter().find(|b| b.block_id == 4).unwrap();
6558 assert_eq!(block4.incoming_edges.len(), 1);
6559 assert_eq!(block4.incoming_edges[0].from_block, 3);
6560 assert_eq!(block4.incoming_edges[0].edge_type, "Fallthrough");
6561 }
6562
6563 #[test]
6565 fn test_unreachable_show_branches_json_output() {
6566 use crate::cfg::reachability::find_unreachable;
6567 use crate::output::JsonResponse;
6568 use petgraph::visit::EdgeRef;
6569
6570 let mut g = DiGraph::new();
6572
6573 let b0 = g.add_node(BasicBlock {
6574 id: 0,
6575 db_id: None,
6576 kind: BlockKind::Entry,
6577 statements: vec!["let x = 1".to_string()],
6578 terminator: Terminator::Goto { target: 1 },
6579 source_location: None,
6580 coord_x: 0,
6581 coord_y: 0,
6582 coord_z: 0,
6583 });
6584
6585 let b1 = g.add_node(BasicBlock {
6586 id: 1,
6587 db_id: None,
6588 kind: BlockKind::Normal,
6589 statements: vec!["if x > 0".to_string()],
6590 terminator: Terminator::SwitchInt {
6591 targets: vec![2],
6592 otherwise: 3,
6593 },
6594 source_location: None,
6595 coord_x: 0,
6596 coord_y: 0,
6597 coord_z: 0,
6598 });
6599
6600 let b2 = g.add_node(BasicBlock {
6601 id: 2,
6602 db_id: None,
6603 kind: BlockKind::Exit,
6604 statements: vec!["return true".to_string()],
6605 terminator: Terminator::Return,
6606 source_location: None,
6607 coord_x: 0,
6608 coord_y: 0,
6609 coord_z: 0,
6610 });
6611
6612 let b3 = g.add_node(BasicBlock {
6613 id: 3,
6614 db_id: None,
6615 kind: BlockKind::Normal,
6616 statements: vec!["unreachable branch".to_string()],
6617 terminator: Terminator::Goto { target: 4 },
6618 source_location: None,
6619 coord_x: 0,
6620 coord_y: 0,
6621 coord_z: 0,
6622 });
6623
6624 let b4 = g.add_node(BasicBlock {
6625 id: 4,
6626 db_id: None,
6627 kind: BlockKind::Exit,
6628 statements: vec!["unreachable code".to_string()],
6629 terminator: Terminator::Unreachable,
6630 source_location: None,
6631 coord_x: 0,
6632 coord_y: 0,
6633 coord_z: 0,
6634 });
6635
6636 g.add_edge(b0, b1, EdgeType::Fallthrough);
6637 g.add_edge(b1, b2, EdgeType::TrueBranch);
6638 g.add_edge(b3, b4, EdgeType::Fallthrough);
6639
6640 let unreachable_indices = find_unreachable(&g);
6642 let blocks: Vec<UnreachableBlock> = unreachable_indices
6643 .iter()
6644 .map(|&idx| {
6645 let block = &g[idx];
6646 UnreachableBlock {
6647 block_id: block.id,
6648 kind: format!("{:?}", block.kind),
6649 statements: block.statements.clone(),
6650 terminator: format!("{:?}", block.terminator),
6651 incoming_edges: g
6652 .edge_references()
6653 .filter(|edge| edge.target() == idx)
6654 .map(|edge| {
6655 let source_block = &g[edge.source()];
6656 let edge_type = g.edge_weight(edge.id()).unwrap();
6657 IncomingEdge {
6658 from_block: source_block.id,
6659 edge_type: format!("{:?}", edge_type),
6660 }
6661 })
6662 .collect(),
6663 }
6664 })
6665 .collect();
6666
6667 let response = UnreachableResponse {
6668 function: "test".to_string(),
6669 total_functions: 1,
6670 functions_with_unreachable: 1,
6671 unreachable_count: 2,
6672 blocks,
6673 uncalled_functions: None,
6674 };
6675
6676 let wrapper = JsonResponse::new(response);
6677 let json = wrapper.to_json();
6678
6679 assert!(json.contains("\"incoming_edges\""));
6681 assert!(json.contains("\"from_block\":3"));
6683 assert!(json.contains("\"edge_type\":\"Fallthrough\""));
6684 }
6685
6686 #[test]
6688 fn test_incoming_edge_serialization() {
6689 let edge = IncomingEdge {
6690 from_block: 5,
6691 edge_type: "TrueBranch".to_string(),
6692 };
6693
6694 let serialized = serde_json::to_string(&edge).unwrap();
6695 assert!(serialized.contains("\"from_block\":5"));
6696 assert!(serialized.contains("\"edge_type\":\"TrueBranch\""));
6697 }
6698}
6699
6700#[cfg(test)]
6705mod dominators_tests {
6706 use super::*;
6707 use crate::cfg::{DominatorTree, PostDominatorTree};
6708 use tempfile::NamedTempFile;
6709
6710 fn create_minimal_db() -> anyhow::Result<NamedTempFile> {
6712 use crate::storage::{
6713 REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
6714 };
6715 let file = NamedTempFile::new()?;
6716 let conn = rusqlite::Connection::open(file.path())?;
6717
6718 conn.execute(
6720 "CREATE TABLE magellan_meta (
6721 id INTEGER PRIMARY KEY CHECK (id = 1),
6722 magellan_schema_version INTEGER NOT NULL,
6723 sqlitegraph_schema_version INTEGER NOT NULL,
6724 created_at INTEGER NOT NULL
6725 )",
6726 [],
6727 )?;
6728
6729 conn.execute(
6730 "CREATE TABLE graph_entities (
6731 id INTEGER PRIMARY KEY AUTOINCREMENT,
6732 kind TEXT NOT NULL,
6733 name TEXT NOT NULL,
6734 file_path TEXT,
6735 data TEXT NOT NULL
6736 )",
6737 [],
6738 )?;
6739
6740 conn.execute(
6741 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
6742 VALUES (1, ?, ?, ?)",
6743 rusqlite::params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
6744 )?;
6745
6746 conn.execute(
6748 "CREATE TABLE mirage_meta (
6749 id INTEGER PRIMARY KEY CHECK (id = 1),
6750 mirage_schema_version INTEGER NOT NULL,
6751 magellan_schema_version INTEGER NOT NULL,
6752 created_at INTEGER NOT NULL
6753 )",
6754 [],
6755 )?;
6756
6757 conn.execute(
6758 "CREATE TABLE cfg_blocks (
6759 id INTEGER PRIMARY KEY AUTOINCREMENT,
6760 function_id INTEGER NOT NULL,
6761 kind TEXT NOT NULL,
6762 byte_start INTEGER NOT NULL,
6763 byte_end INTEGER NOT NULL,
6764 terminator TEXT NOT NULL,
6765 function_hash TEXT NOT NULL
6766 )",
6767 [],
6768 )?;
6769
6770 conn.execute(
6773 "CREATE TABLE cfg_paths (
6774 path_id TEXT PRIMARY KEY,
6775 function_id INTEGER NOT NULL,
6776 path_kind TEXT NOT NULL,
6777 entry_block INTEGER NOT NULL,
6778 exit_block INTEGER NOT NULL,
6779 length INTEGER NOT NULL,
6780 created_at INTEGER NOT NULL
6781 )",
6782 [],
6783 )?;
6784
6785 conn.execute(
6786 "CREATE TABLE cfg_dominators (
6787 id INTEGER PRIMARY KEY AUTOINCREMENT,
6788 block_id INTEGER NOT NULL,
6789 dominator_id INTEGER NOT NULL,
6790 is_strict INTEGER NOT NULL
6791 )",
6792 [],
6793 )?;
6794
6795 conn.execute(
6796 "INSERT INTO mirage_meta (id, mirage_schema_version, magellan_schema_version, created_at)
6797 VALUES (1, 1, 4, 0)",
6798 [],
6799 )?;
6800
6801 conn.execute(
6803 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
6804 rusqlite::params!("function", "test_func", "test.rs", "{}"),
6805 )?;
6806
6807 Ok(file)
6808 }
6809
6810 #[test]
6812 fn test_dominator_tree_computation() {
6813 let cfg = cmds::create_test_cfg();
6814 let dom_tree = DominatorTree::new(&cfg);
6815
6816 assert!(
6817 dom_tree.is_some(),
6818 "DominatorTree should be computed successfully"
6819 );
6820
6821 let dom_tree = dom_tree.unwrap();
6822 assert_eq!(cfg[dom_tree.root()].id, 0, "Root should be entry block");
6824 }
6825
6826 #[test]
6828 fn test_post_dominator_tree_computation() {
6829 let cfg = cmds::create_test_cfg();
6830 let post_dom_tree = PostDominatorTree::new(&cfg);
6831
6832 assert!(
6833 post_dom_tree.is_some(),
6834 "PostDominatorTree should be computed successfully"
6835 );
6836
6837 let post_dom_tree = post_dom_tree.unwrap();
6838 let root_id = cfg[post_dom_tree.root()].id;
6840 assert!(root_id == 2 || root_id == 3, "Root should be an exit block");
6841 }
6842
6843 #[test]
6845 fn test_immediate_dominator_relationships() {
6846 let cfg = cmds::create_test_cfg();
6847 let dom_tree = DominatorTree::new(&cfg).unwrap();
6848
6849 let node_0 = cfg.node_indices().find(|&n| cfg[n].id == 0).unwrap();
6851 let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
6852 let node_2 = cfg.node_indices().find(|&n| cfg[n].id == 2).unwrap();
6853 let node_3 = cfg.node_indices().find(|&n| cfg[n].id == 3).unwrap();
6854
6855 assert_eq!(
6857 dom_tree.immediate_dominator(node_0),
6858 None,
6859 "Entry should have no dominator"
6860 );
6861
6862 assert_eq!(
6864 dom_tree.immediate_dominator(node_1),
6865 Some(node_0),
6866 "Node 1 should be dominated by entry"
6867 );
6868
6869 assert_eq!(
6871 dom_tree.immediate_dominator(node_2),
6872 Some(node_1),
6873 "Node 2 should be dominated by node 1"
6874 );
6875
6876 assert_eq!(
6878 dom_tree.immediate_dominator(node_3),
6879 Some(node_1),
6880 "Node 3 should be dominated by node 1"
6881 );
6882 }
6883
6884 #[test]
6886 fn test_dominates_method() {
6887 let cfg = cmds::create_test_cfg();
6888 let dom_tree = DominatorTree::new(&cfg).unwrap();
6889
6890 let node_0 = cfg.node_indices().find(|&n| cfg[n].id == 0).unwrap();
6891 let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
6892 let node_2 = cfg.node_indices().find(|&n| cfg[n].id == 2).unwrap();
6893
6894 assert!(dom_tree.dominates(node_0, node_0), "Node dominates itself");
6896 assert!(dom_tree.dominates(node_0, node_1), "Entry dominates node 1");
6897 assert!(dom_tree.dominates(node_0, node_2), "Entry dominates node 2");
6898
6899 assert!(
6901 !dom_tree.dominates(node_1, node_0),
6902 "Node 1 does not dominate entry"
6903 );
6904 }
6905
6906 #[test]
6908 fn test_dominator_tree_children() {
6909 let cfg = cmds::create_test_cfg();
6910 let dom_tree = DominatorTree::new(&cfg).unwrap();
6911
6912 let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
6913
6914 let children = dom_tree.children(node_1);
6916 assert_eq!(children.len(), 2, "Node 1 should have 2 children");
6917
6918 let child_ids: Vec<_> = children.iter().map(|&n| cfg[n].id).collect();
6919 assert!(child_ids.contains(&2), "Children should include block 2");
6920 assert!(child_ids.contains(&3), "Children should include block 3");
6921 }
6922
6923 #[test]
6925 fn test_dominators_args_fields() {
6926 let args = DominatorsArgs {
6927 function: "test_func".to_string(),
6928 file: None,
6929 must_pass_through: Some("1".to_string()),
6930 post: false,
6931 inter_procedural: false,
6932 };
6933
6934 assert_eq!(args.function, "test_func");
6935 assert_eq!(args.must_pass_through, Some("1".to_string()));
6936 assert!(!args.post);
6937 assert!(!args.inter_procedural);
6938 }
6939
6940 #[test]
6942 fn test_dominators_args_with_post_flag() {
6943 let args = DominatorsArgs {
6944 function: "my_function".to_string(),
6945 file: None,
6946 must_pass_through: None,
6947 post: true,
6948 inter_procedural: false,
6949 };
6950
6951 assert_eq!(args.function, "my_function");
6952 assert!(args.post, "post flag should be true");
6953 assert!(
6954 args.must_pass_through.is_none(),
6955 "must_pass_through should be None"
6956 );
6957 assert!(!args.inter_procedural);
6958 }
6959
6960 #[test]
6962 fn test_dominance_response_serialization() {
6963 let response = DominanceResponse {
6964 function: "test".to_string(),
6965 kind: "dominators".to_string(),
6966 root: Some(0),
6967 dominance_tree: vec![DominatorEntry {
6968 block: 0,
6969 immediate_dominator: None,
6970 dominated: vec![1],
6971 }],
6972 must_pass_through: None,
6973 };
6974
6975 let json = serde_json::to_string(&response);
6976 assert!(json.is_ok(), "DominanceResponse should serialize to JSON");
6977
6978 let json_str = json.unwrap();
6979 assert!(json_str.contains("\"function\":\"test\""));
6980 assert!(json_str.contains("\"kind\":\"dominators\""));
6981 assert!(json_str.contains("\"root\":0"));
6982 }
6983
6984 #[test]
6986 fn test_must_pass_through_result() {
6987 let result = MustPassThroughResult {
6988 block: 1,
6989 must_pass: vec![1, 2, 3],
6990 };
6991
6992 assert_eq!(result.block, 1);
6993 assert_eq!(result.must_pass.len(), 3);
6994 assert_eq!(result.must_pass, vec![1, 2, 3]);
6995
6996 let json = serde_json::to_string(&result);
6998 assert!(json.is_ok());
6999 let json_str = json.unwrap();
7000 assert!(json_str.contains("\"block\":1"));
7001 assert!(json_str.contains("\"must_pass\":[1,2,3]"));
7002 }
7003
7004 #[test]
7006 fn test_dominator_entry() {
7007 let entry = DominatorEntry {
7008 block: 5,
7009 immediate_dominator: Some(2),
7010 dominated: vec![6, 7],
7011 };
7012
7013 assert_eq!(entry.block, 5);
7014 assert_eq!(entry.immediate_dominator, Some(2));
7015 assert_eq!(entry.dominated, vec![6, 7]);
7016 }
7017
7018 #[test]
7020 fn test_post_dominates_method() {
7021 let cfg = cmds::create_test_cfg();
7022 let post_dom_tree = PostDominatorTree::new(&cfg).unwrap();
7023
7024 let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
7025 let node_2 = cfg.node_indices().find(|&n| cfg[n].id == 2).unwrap();
7026
7027 assert!(
7029 post_dom_tree.post_dominates(node_2, node_2),
7030 "Node post-dominates itself"
7031 );
7032 assert!(
7033 post_dom_tree.post_dominates(node_2, node_1),
7034 "Exit post-dominates node 1"
7035 );
7036 }
7037
7038 #[test]
7040 fn test_immediate_post_dominator_relationships() {
7041 let cfg = cmds::create_test_cfg();
7042 let post_dom_tree = PostDominatorTree::new(&cfg).unwrap();
7043
7044 let node_0 = cfg.node_indices().find(|&n| cfg[n].id == 0).unwrap();
7045 let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
7046
7047 let ipdom_1 = post_dom_tree.immediate_post_dominator(node_1);
7049 assert!(
7050 ipdom_1.is_some(),
7051 "Node 1 should have an immediate post-dominator"
7052 );
7053
7054 let ipdom_0 = post_dom_tree.immediate_post_dominator(node_0);
7056 assert_eq!(
7057 ipdom_0,
7058 Some(node_1),
7059 "Node 0 should be immediately post-dominated by node 1"
7060 );
7061 }
7062
7063 #[test]
7065 fn test_empty_cfg_dominator_tree() {
7066 use petgraph::graph::DiGraph;
7067 let empty_cfg: crate::cfg::Cfg = DiGraph::new();
7068 let dom_tree = DominatorTree::new(&empty_cfg);
7069
7070 assert!(
7071 dom_tree.is_none(),
7072 "Empty CFG should produce None for DominatorTree"
7073 );
7074 }
7075
7076 #[test]
7078 fn test_empty_cfg_post_dominator_tree() {
7079 use petgraph::graph::DiGraph;
7080 let empty_cfg: crate::cfg::Cfg = DiGraph::new();
7081 let post_dom_tree = PostDominatorTree::new(&empty_cfg);
7082
7083 assert!(
7084 post_dom_tree.is_none(),
7085 "Empty CFG should produce None for PostDominatorTree"
7086 );
7087 }
7088
7089 #[test]
7091 fn test_dominance_response_json_wrapper() {
7092 use crate::output::JsonResponse;
7093
7094 let response = DominanceResponse {
7095 function: "wrapped_test".to_string(),
7096 kind: "dominators".to_string(),
7097 root: Some(0),
7098 dominance_tree: vec![],
7099 must_pass_through: None,
7100 };
7101
7102 let wrapper = JsonResponse::new(response);
7103
7104 assert_eq!(wrapper.schema_version, "1.0.1");
7105 assert_eq!(wrapper.tool, "mirage");
7106 assert!(!wrapper.execution_id.is_empty());
7107 assert!(!wrapper.timestamp.is_empty());
7108
7109 let json = wrapper.to_json();
7111 assert!(json.contains("\"schema_version\":\"1.0.1\""));
7112 assert!(json.contains("\"tool\":\"mirage\""));
7113 assert!(json.contains("wrapped_test"));
7114 }
7115
7116 #[test]
7118 fn test_must_pass_through_valid_block() {
7119 let cfg = cmds::create_test_cfg();
7120 let dom_tree = DominatorTree::new(&cfg).unwrap();
7121
7122 let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
7123
7124 let must_pass: Vec<usize> = cfg
7126 .node_indices()
7127 .filter(|&n| dom_tree.dominates(node_1, n))
7128 .map(|n| cfg[n].id)
7129 .collect();
7130
7131 assert_eq!(must_pass.len(), 3, "Block 1 should dominate 3 blocks");
7132 assert!(must_pass.contains(&1), "Must include block 1 itself");
7133 assert!(must_pass.contains(&2), "Must include block 2");
7134 assert!(must_pass.contains(&3), "Must include block 3");
7135 }
7136
7137 #[test]
7139 fn test_nonexistent_block_id() {
7140 let cfg = cmds::create_test_cfg();
7141
7142 let found = cfg.node_indices().find(|&n| cfg[n].id == 99);
7144 assert!(found.is_none(), "Non-existent block should not be found");
7145 }
7146
7147 #[test]
7149 fn test_dominators_json_structure() {
7150 use crate::output::JsonResponse;
7151
7152 let response = DominanceResponse {
7153 function: "json_test".to_string(),
7154 kind: "post-dominators".to_string(),
7155 root: Some(3),
7156 dominance_tree: vec![DominatorEntry {
7157 block: 3,
7158 immediate_dominator: None,
7159 dominated: vec![0, 2],
7160 }],
7161 must_pass_through: Some(MustPassThroughResult {
7162 block: 0,
7163 must_pass: vec![0, 1],
7164 }),
7165 };
7166
7167 let wrapper = JsonResponse::new(response);
7168 let json = wrapper.to_json();
7169
7170 assert!(json.contains("\"kind\":\"post-dominators\""));
7171 assert!(json.contains("\"root\":3"));
7172 assert!(json.contains("\"must_pass_through\""));
7173 assert!(json.contains("\"block\":0"));
7174 }
7175}
7176
7177#[cfg(test)]
7182mod verify_tests {
7183 use super::*;
7184 use crate::cfg::{enumerate_paths, PathLimits};
7185 use crate::output::JsonResponse;
7186 use crate::storage::MirageDb;
7187
7188 fn create_test_db_with_cached_path(
7190 ) -> anyhow::Result<(tempfile::NamedTempFile, MirageDb, String)> {
7191 use crate::storage::{
7192 REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
7193 };
7194 let file = tempfile::NamedTempFile::new()?;
7195 let mut conn = rusqlite::Connection::open(file.path())?;
7196
7197 conn.execute(
7199 "CREATE TABLE magellan_meta (
7200 id INTEGER PRIMARY KEY CHECK (id = 1),
7201 magellan_schema_version INTEGER NOT NULL,
7202 sqlitegraph_schema_version INTEGER NOT NULL,
7203 created_at INTEGER NOT NULL
7204 )",
7205 [],
7206 )?;
7207
7208 conn.execute(
7209 "CREATE TABLE graph_entities (
7210 id INTEGER PRIMARY KEY AUTOINCREMENT,
7211 kind TEXT NOT NULL,
7212 name TEXT NOT NULL,
7213 file_path TEXT,
7214 data TEXT NOT NULL
7215 )",
7216 [],
7217 )?;
7218
7219 conn.execute(
7220 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
7221 VALUES (1, ?, ?, ?)",
7222 rusqlite::params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
7223 )?;
7224
7225 crate::storage::create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION)?;
7227
7228 conn.execute(
7230 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
7231 rusqlite::params!("function", "test_func", "test.rs", "{}"),
7232 )?;
7233 let function_id: i64 = conn.last_insert_rowid();
7234
7235 let cfg = cmds::create_test_cfg();
7237 let paths = enumerate_paths(&cfg, &PathLimits::default());
7238
7239 if let Some(first_path) = paths.first() {
7241 let path_id = &first_path.path_id;
7242
7243 conn.execute(
7245 "INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
7246 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
7247 rusqlite::params![
7248 path_id,
7249 function_id,
7250 "Normal",
7251 first_path.entry as i64,
7252 first_path.exit as i64,
7253 first_path.len() as i64,
7254 0,
7255 ],
7256 )?;
7257
7258 for (idx, &block_id) in first_path.blocks.iter().enumerate() {
7260 conn.execute(
7261 "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id)
7262 VALUES (?1, ?2, ?3)",
7263 rusqlite::params![path_id, idx as i64, block_id as i64],
7264 )?;
7265 }
7266
7267 let db = MirageDb::open(file.path())?;
7268 Ok((file, db, path_id.clone()))
7269 } else {
7270 anyhow::bail!("No paths found in test CFG")
7271 }
7272 }
7273
7274 #[test]
7276 #[cfg(feature = "backend-sqlite")]
7277 fn test_verify_valid_path() {
7278 let (_file, _db, cached_path_id) = create_test_db_with_cached_path().unwrap();
7279
7280 let cfg = cmds::create_test_cfg();
7282 let current_paths = enumerate_paths(&cfg, &PathLimits::default());
7283
7284 let is_valid = current_paths.iter().any(|p| p.path_id == cached_path_id);
7286
7287 assert!(is_valid, "Cached path should exist in current enumeration");
7289 }
7290
7291 #[test]
7293 fn test_verify_result_serialization() {
7294 let result = VerifyResult {
7295 path_id: "test_path_123".to_string(),
7296 valid: true,
7297 found_in_cache: true,
7298 function_id: Some(1),
7299 reason: "Path found in current enumeration".to_string(),
7300 current_paths: 2,
7301 };
7302
7303 let json = serde_json::to_string(&result);
7304 assert!(json.is_ok());
7305
7306 let json_str = json.unwrap();
7307 assert!(json_str.contains("\"path_id\":\"test_path_123\""));
7308 assert!(json_str.contains("\"valid\":true"));
7309 assert!(json_str.contains("\"found_in_cache\":true"));
7310 assert!(json_str.contains("\"function_id\":1"));
7311 assert!(json_str.contains("\"reason\""));
7312 assert!(json_str.contains("\"current_paths\":2"));
7313 }
7314
7315 #[test]
7317 fn test_verify_invalid_path_result() {
7318 let result = VerifyResult {
7319 path_id: "nonexistent_path".to_string(),
7320 valid: false,
7321 found_in_cache: false,
7322 function_id: None,
7323 reason: "Path not found in cache".to_string(),
7324 current_paths: 0,
7325 };
7326
7327 assert!(!result.valid);
7328 assert!(!result.found_in_cache);
7329 assert!(result.function_id.is_none());
7330 assert_eq!(result.reason, "Path not found in cache");
7331 }
7332
7333 #[test]
7335 fn test_verify_args_fields() {
7336 let args = VerifyArgs {
7337 path_id: "abc123".to_string(),
7338 };
7339
7340 assert_eq!(args.path_id, "abc123");
7341 }
7342
7343 #[test]
7345 fn test_verify_result_json_wrapper() {
7346 let result = VerifyResult {
7347 path_id: "wrapped_path".to_string(),
7348 valid: true,
7349 found_in_cache: true,
7350 function_id: Some(42),
7351 reason: "Test reason".to_string(),
7352 current_paths: 100,
7353 };
7354
7355 let wrapper = JsonResponse::new(result);
7356
7357 assert_eq!(wrapper.schema_version, "1.0.1");
7358 assert_eq!(wrapper.tool, "mirage");
7359 assert!(!wrapper.execution_id.is_empty());
7360 assert!(!wrapper.timestamp.is_empty());
7361
7362 let json = wrapper.to_json();
7363 assert!(json.contains("\"schema_version\":\"1.0.1\""));
7364 assert!(json.contains("\"tool\":\"mirage\""));
7365 assert!(json.contains("wrapped_path"));
7366 }
7367
7368 #[test]
7370 fn test_verify_check_path_exists() {
7371 let cfg = cmds::create_test_cfg();
7372 let paths = enumerate_paths(&cfg, &PathLimits::default());
7373
7374 if let Some(first_path) = paths.first() {
7376 let path_id = &first_path.path_id;
7377
7378 let exists = paths.iter().any(|p| &p.path_id == path_id);
7380 assert!(exists, "Path should exist in enumeration");
7381
7382 let same_blocks = paths.iter().any(|p| p.blocks == first_path.blocks);
7384 assert!(same_blocks, "Should find path with same blocks");
7385 }
7386 }
7387
7388 #[test]
7390 fn test_verify_multiple_paths_have_different_ids() {
7391 let cfg = cmds::create_test_cfg();
7392 let paths = enumerate_paths(&cfg, &PathLimits::default());
7393
7394 assert!(paths.len() >= 2, "Test CFG should have at least 2 paths");
7396
7397 let mut path_ids = std::collections::HashSet::new();
7399 for path in &paths {
7400 assert!(
7401 path_ids.insert(&path.path_id),
7402 "Path ID should be unique: {}",
7403 path.path_id
7404 );
7405 }
7406 }
7407
7408 #[test]
7410 fn test_verify_path_not_in_cache() {
7411 let result = VerifyResult {
7412 path_id: "fake_id_that_does_not_exist".to_string(),
7413 valid: false,
7414 found_in_cache: false,
7415 function_id: None,
7416 reason: "Path not found in cache".to_string(),
7417 current_paths: 0,
7418 };
7419
7420 assert!(!result.found_in_cache);
7421 assert!(!result.valid);
7422 }
7423
7424 #[test]
7426 fn test_verify_json_output_format() {
7427 let result = VerifyResult {
7428 path_id: "json_test_path".to_string(),
7429 valid: true,
7430 found_in_cache: true,
7431 function_id: Some(123),
7432 reason: "Test".to_string(),
7433 current_paths: 5,
7434 };
7435
7436 let wrapper = JsonResponse::new(result);
7437 let json = wrapper.to_pretty_json();
7438
7439 assert!(json.contains("\n"));
7441
7442 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
7444 assert_eq!(parsed["tool"], "mirage");
7445 assert_eq!(parsed["data"]["path_id"], "json_test_path");
7446 assert_eq!(parsed["data"]["valid"], true);
7447 }
7448
7449 #[test]
7451 fn test_verify_result_without_function_id() {
7452 let result = VerifyResult {
7453 path_id: "orphan_path".to_string(),
7454 valid: false,
7455 found_in_cache: false,
7456 function_id: None,
7457 reason: "No function associated".to_string(),
7458 current_paths: 10,
7459 };
7460
7461 let json = serde_json::to_string(&result).unwrap();
7462 assert!(json.contains("\"function_id\":null"));
7463 assert!(!result.valid);
7464 assert!(!result.found_in_cache);
7465 }
7466}
7467
7468#[cfg(test)]
7473mod output_format_tests {
7474 use super::*;
7475 use crate::output::JsonResponse;
7476
7477 #[test]
7479 fn test_all_response_types_serialize() {
7480 let paths_resp = PathsResponse {
7482 function: "test_func".to_string(),
7483 total_paths: 2,
7484 error_paths: 0,
7485 paths: vec![],
7486 };
7487 let paths_json = serde_json::to_string(&paths_resp);
7488 assert!(paths_json.is_ok(), "PathsResponse should serialize");
7489
7490 let dom_resp = DominanceResponse {
7492 function: "test_func".to_string(),
7493 kind: "dominators".to_string(),
7494 root: Some(0),
7495 dominance_tree: vec![],
7496 must_pass_through: None,
7497 };
7498 let dom_json = serde_json::to_string(&dom_resp);
7499 assert!(dom_json.is_ok(), "DominanceResponse should serialize");
7500
7501 let unreach_resp = UnreachableResponse {
7503 uncalled_functions: None,
7504 function: "test_func".to_string(),
7505 total_functions: 1,
7506 functions_with_unreachable: 0,
7507 unreachable_count: 0,
7508 blocks: vec![],
7509 };
7510 let unreach_json = serde_json::to_string(&unreach_resp);
7511 assert!(unreach_json.is_ok(), "UnreachableResponse should serialize");
7512
7513 let verify_res = VerifyResult {
7515 path_id: "test_path".to_string(),
7516 valid: true,
7517 found_in_cache: true,
7518 function_id: Some(1),
7519 reason: "Test".to_string(),
7520 current_paths: 2,
7521 };
7522 let verify_json = serde_json::to_string(&verify_res);
7523 assert!(verify_json.is_ok(), "VerifyResult should serialize");
7524 }
7525
7526 #[test]
7528 fn test_json_response_wrapper_for_all_commands() {
7529 let paths_resp = PathsResponse {
7531 function: "test_func".to_string(),
7532 total_paths: 2,
7533 error_paths: 0,
7534 paths: vec![],
7535 };
7536 let paths_wrapper = JsonResponse::new(paths_resp);
7537 assert_eq!(paths_wrapper.schema_version, "1.0.1");
7538 assert_eq!(paths_wrapper.tool, "mirage");
7539 assert!(!paths_wrapper.execution_id.is_empty());
7540
7541 let dom_resp = DominanceResponse {
7543 function: "test_func".to_string(),
7544 kind: "dominators".to_string(),
7545 root: Some(0),
7546 dominance_tree: vec![],
7547 must_pass_through: None,
7548 };
7549 let dom_wrapper = JsonResponse::new(dom_resp);
7550 assert_eq!(dom_wrapper.schema_version, "1.0.1");
7551 assert_eq!(dom_wrapper.tool, "mirage");
7552
7553 let unreach_resp = UnreachableResponse {
7555 uncalled_functions: None,
7556 function: "test_func".to_string(),
7557 total_functions: 1,
7558 functions_with_unreachable: 0,
7559 unreachable_count: 0,
7560 blocks: vec![],
7561 };
7562 let unreach_wrapper = JsonResponse::new(unreach_resp);
7563 assert_eq!(unreach_wrapper.schema_version, "1.0.1");
7564 assert_eq!(unreach_wrapper.tool, "mirage");
7565
7566 let verify_res = VerifyResult {
7568 path_id: "test_path".to_string(),
7569 valid: true,
7570 found_in_cache: true,
7571 function_id: Some(1),
7572 reason: "Test".to_string(),
7573 current_paths: 2,
7574 };
7575 let verify_wrapper = JsonResponse::new(verify_res);
7576 assert_eq!(verify_wrapper.schema_version, "1.0.1");
7577 assert_eq!(verify_wrapper.tool, "mirage");
7578 }
7579
7580 #[test]
7582 fn test_json_response_compact_format() {
7583 let data = vec!["item1", "item2"];
7584 let wrapper = JsonResponse::new(data);
7585 let compact = wrapper.to_json();
7586
7587 assert!(
7589 !compact.contains("\n"),
7590 "Compact JSON should not have newlines"
7591 );
7592 assert!(
7593 compact.contains("\"item1\""),
7594 "Compact JSON should contain data"
7595 );
7596 }
7597
7598 #[test]
7600 fn test_json_response_pretty_format() {
7601 let data = vec!["item1", "item2"];
7602 let wrapper = JsonResponse::new(data);
7603 let pretty = wrapper.to_pretty_json();
7604
7605 assert!(pretty.contains("\n"), "Pretty JSON should have newlines");
7607 assert!(pretty.contains(" "), "Pretty JSON should have indentation");
7608
7609 let compact = wrapper.to_json();
7611 let compact_val: serde_json::Value = serde_json::from_str(&compact).unwrap();
7612 let pretty_val: serde_json::Value = serde_json::from_str(&pretty).unwrap();
7613 assert_eq!(
7614 compact_val, pretty_val,
7615 "Both formats should produce same data"
7616 );
7617 }
7618
7619 #[test]
7621 fn test_json_response_required_fields() {
7622 let data = "test_data";
7623 let wrapper = JsonResponse::new(data);
7624
7625 assert_eq!(wrapper.schema_version, "1.0.1");
7627 assert_eq!(wrapper.tool, "mirage");
7628 assert!(!wrapper.execution_id.is_empty());
7629 assert!(!wrapper.timestamp.is_empty());
7630
7631 assert!(
7633 wrapper.execution_id.contains("-"),
7634 "execution_id should contain hyphen"
7635 );
7636
7637 let parsed_time = chrono::DateTime::parse_from_rfc3339(&wrapper.timestamp);
7639 assert!(parsed_time.is_ok(), "timestamp should be valid RFC3339");
7640 }
7641
7642 #[test]
7644 fn test_output_format_enum_matches() {
7645 assert_ne!(OutputFormat::Human, OutputFormat::Json);
7647 assert_ne!(OutputFormat::Human, OutputFormat::Pretty);
7648 assert_ne!(OutputFormat::Json, OutputFormat::Pretty);
7649
7650 assert_eq!(OutputFormat::Human, OutputFormat::Human);
7652 assert_eq!(OutputFormat::Json, OutputFormat::Json);
7653 assert_eq!(OutputFormat::Pretty, OutputFormat::Pretty);
7654 }
7655
7656 #[test]
7658 fn test_human_output_no_json_artifacts() {
7659 let function_name = "test_function";
7663 let path_count = 5;
7664
7665 let mut output = String::new();
7667 output.push_str(&format!("Function: {}\n", function_name));
7668 output.push_str(&format!("Total paths: {}\n", path_count));
7669
7670 assert!(
7672 !output.contains("{"),
7673 "Human output should not contain JSON objects"
7674 );
7675 assert!(
7676 !output.contains("}"),
7677 "Human output should not contain JSON objects"
7678 );
7679 assert!(
7680 !output.contains("\""),
7681 "Human output should not contain JSON quotes"
7682 );
7683 assert!(
7684 !output.contains("schema_version"),
7685 "Human output should not contain JSON metadata"
7686 );
7687 }
7688
7689 #[test]
7691 fn test_json_output_has_metadata() {
7692 let data = "test_data";
7693 let wrapper = JsonResponse::new(data);
7694 let json = wrapper.to_json();
7695
7696 assert!(json.contains("\"schema_version\""));
7698 assert!(json.contains("\"execution_id\""));
7699 assert!(json.contains("\"tool\""));
7700 assert!(json.contains("\"timestamp\""));
7701 assert!(json.contains("\"data\""));
7702 }
7703
7704 #[test]
7706 fn test_error_response_format() {
7707 use crate::output::JsonError;
7708
7709 let error = JsonError::new("category", "message", "CODE");
7710 assert_eq!(error.error, "category");
7711 assert_eq!(error.message, "message");
7712 assert_eq!(error.code, "CODE");
7713 assert!(error.remediation.is_none());
7714
7715 let error_with_remediation = error.with_remediation("Try X instead");
7716 assert_eq!(
7717 error_with_remediation.remediation,
7718 Some("Try X instead".to_string())
7719 );
7720
7721 let json = serde_json::to_string(&error_with_remediation);
7723 assert!(json.is_ok());
7724 let json_str = json.unwrap();
7725 assert!(json_str.contains("\"error\""));
7726 assert!(json_str.contains("\"message\""));
7727 assert!(json_str.contains("\"code\""));
7728 assert!(json_str.contains("\"remediation\""));
7729 }
7730
7731 #[test]
7733 fn test_cli_with_different_output_formats() {
7734 let formats = vec![
7735 OutputFormat::Human,
7736 OutputFormat::Json,
7737 OutputFormat::Pretty,
7738 ];
7739
7740 for format in formats {
7741 let cli = Cli {
7742 db: Some("./test.db".to_string()),
7743 output: format,
7744 command: Some(Commands::Status(StatusArgs {})),
7745 detect_backend: false,
7746 };
7747
7748 assert_eq!(cli.output, format);
7749 assert_eq!(cli.db, Some("./test.db".to_string()));
7750 }
7751 }
7752
7753 #[test]
7755 fn test_cfg_format_enum() {
7756 let formats = vec![CfgFormat::Human, CfgFormat::Dot, CfgFormat::Json];
7757
7758 for format in &formats {
7759 match format {
7760 CfgFormat::Human => assert!(true),
7761 CfgFormat::Dot => assert!(true),
7762 CfgFormat::Json => assert!(true),
7763 }
7764 }
7765
7766 assert_ne!(CfgFormat::Human, CfgFormat::Dot);
7768 assert_ne!(CfgFormat::Human, CfgFormat::Json);
7769 assert_ne!(CfgFormat::Dot, CfgFormat::Json);
7770 }
7771
7772 #[test]
7774 fn test_response_snake_case_naming() {
7775 let paths_resp = PathsResponse {
7777 function: "test".to_string(),
7778 total_paths: 1,
7779 error_paths: 0,
7780 paths: vec![],
7781 };
7782 let json = serde_json::to_string(&paths_resp).unwrap();
7783
7784 assert!(json.contains("\"function\""));
7786 assert!(json.contains("\"total_paths\""));
7787 assert!(json.contains("\"error_paths\""));
7788
7789 assert!(!json.contains("\"totalPaths\""));
7791 assert!(!json.contains("\"errorPaths\""));
7792 }
7793
7794 #[test]
7796 fn test_loops_detects_loops() {
7797 use crate::cfg::{detect_natural_loops, BasicBlock, BlockKind, EdgeType, Terminator};
7798 use petgraph::graph::DiGraph;
7799
7800 let mut g = DiGraph::new();
7802
7803 let b0 = g.add_node(BasicBlock {
7804 id: 0,
7805 db_id: None,
7806 kind: BlockKind::Entry,
7807 statements: vec![],
7808 terminator: Terminator::Goto { target: 1 },
7809 source_location: None,
7810 coord_x: 0,
7811 coord_y: 0,
7812 coord_z: 0,
7813 });
7814
7815 let b1 = g.add_node(BasicBlock {
7816 id: 1,
7817 db_id: None,
7818 kind: BlockKind::Normal,
7819 statements: vec![],
7820 terminator: Terminator::SwitchInt {
7821 targets: vec![2],
7822 otherwise: 3,
7823 },
7824 source_location: None,
7825 coord_x: 0,
7826 coord_y: 0,
7827 coord_z: 0,
7828 });
7829
7830 let b2 = g.add_node(BasicBlock {
7831 id: 2,
7832 db_id: None,
7833 kind: BlockKind::Normal,
7834 statements: vec!["loop body".to_string()],
7835 terminator: Terminator::Goto { target: 1 },
7836 source_location: None,
7837 coord_x: 0,
7838 coord_y: 0,
7839 coord_z: 0,
7840 });
7841
7842 let b3 = g.add_node(BasicBlock {
7843 id: 3,
7844 db_id: None,
7845 kind: BlockKind::Exit,
7846 statements: vec![],
7847 terminator: Terminator::Return,
7848 source_location: None,
7849 coord_x: 0,
7850 coord_y: 0,
7851 coord_z: 0,
7852 });
7853
7854 g.add_edge(b0, b1, EdgeType::Fallthrough);
7855 g.add_edge(b1, b2, EdgeType::TrueBranch);
7856 g.add_edge(b1, b3, EdgeType::FalseBranch);
7857 g.add_edge(b2, b1, EdgeType::LoopBack);
7858
7859 let loops = detect_natural_loops(&g);
7860
7861 assert_eq!(loops.len(), 1, "Should detect exactly one loop");
7863 assert_eq!(loops[0].header.index(), 1, "Loop header should be block 1");
7864 }
7865
7866 #[test]
7868 fn test_loops_empty_cfg() {
7869 use crate::cfg::detect_natural_loops;
7870 use petgraph::graph::DiGraph;
7871 let empty_cfg: crate::cfg::Cfg = DiGraph::new();
7872 let loops = detect_natural_loops(&empty_cfg);
7873
7874 assert!(loops.is_empty(), "Empty CFG should have no loops");
7875 }
7876
7877 #[test]
7879 fn test_loops_response_serialization() {
7880 use crate::output::JsonResponse;
7881
7882 let response = LoopsResponse {
7883 function: "test_func".to_string(),
7884 loop_count: 2,
7885 loops: vec![
7886 LoopInfo {
7887 header: 1,
7888 back_edge_from: 2,
7889 body_size: 2,
7890 nesting_level: 0,
7891 body_blocks: vec![1, 2],
7892 },
7893 LoopInfo {
7894 header: 3,
7895 back_edge_from: 4,
7896 body_size: 3,
7897 nesting_level: 1,
7898 body_blocks: vec![1, 2, 3],
7899 },
7900 ],
7901 };
7902
7903 let json = serde_json::to_string(&response).unwrap();
7905 assert!(json.contains("\"function\""));
7906 assert!(json.contains("\"loop_count\""));
7907 assert!(json.contains("\"loops\""));
7908
7909 let wrapper = JsonResponse::new(response);
7911 let wrapped_json = wrapper.to_json();
7912 assert!(wrapped_json.contains("\"schema_version\""));
7913 assert!(wrapped_json.contains("\"execution_id\""));
7914 }
7915
7916 #[test]
7918 fn test_loops_args_fields() {
7919 let args = LoopsArgs {
7920 function: "my_function".to_string(),
7921 file: None,
7922 verbose: true,
7923 };
7924
7925 assert_eq!(args.function, "my_function");
7926 assert!(args.verbose);
7927 }
7928
7929 #[test]
7931 fn test_loop_info_fields() {
7932 let loop_info = LoopInfo {
7933 header: 5,
7934 back_edge_from: 7,
7935 body_size: 3,
7936 nesting_level: 2,
7937 body_blocks: vec![5, 6, 7],
7938 };
7939
7940 assert_eq!(loop_info.header, 5);
7941 assert_eq!(loop_info.back_edge_from, 7);
7942 assert_eq!(loop_info.body_size, 3);
7943 assert_eq!(loop_info.nesting_level, 2);
7944 assert_eq!(loop_info.body_blocks, vec![5, 6, 7]);
7945 }
7946
7947 #[test]
7949 fn test_loops_json_output_format() {
7950 use crate::output::JsonResponse;
7951
7952 let response = LoopsResponse {
7953 function: "json_test".to_string(),
7954 loop_count: 1,
7955 loops: vec![LoopInfo {
7956 header: 1,
7957 back_edge_from: 2,
7958 body_size: 2,
7959 nesting_level: 0,
7960 body_blocks: vec![1, 2],
7961 }],
7962 };
7963
7964 let wrapper = JsonResponse::new(response);
7965 let json = wrapper.to_json();
7966
7967 assert!(json.contains("\"schema_version\""));
7969 assert!(json.contains("\"execution_id\""));
7970 assert!(json.contains("\"tool\""));
7971 assert!(json.contains("\"timestamp\""));
7972 assert!(json.contains("\"data\""));
7973 }
7974
7975 #[test]
7977 fn test_loops_verbose_flag() {
7978 let args_verbose = LoopsArgs {
7979 function: "test".to_string(),
7980 file: None,
7981 verbose: true,
7982 };
7983
7984 let args_not_verbose = LoopsArgs {
7985 function: "test".to_string(),
7986 file: None,
7987 verbose: false,
7988 };
7989
7990 assert!(args_verbose.verbose);
7991 assert!(!args_not_verbose.verbose);
7992 }
7993
7994 #[test]
7996 fn test_loops_nesting_levels() {
7997 let loop_outer = LoopInfo {
7998 header: 1,
7999 back_edge_from: 3,
8000 body_size: 3,
8001 nesting_level: 0, body_blocks: vec![1, 2, 3],
8003 };
8004
8005 let loop_inner = LoopInfo {
8006 header: 2,
8007 back_edge_from: 4,
8008 body_size: 2,
8009 nesting_level: 1, body_blocks: vec![2, 4],
8011 };
8012
8013 assert_eq!(loop_outer.nesting_level, 0);
8014 assert_eq!(loop_inner.nesting_level, 1);
8015 }
8016
8017 #[test]
8019 fn test_loops_response_empty() {
8020 use crate::output::JsonResponse;
8021
8022 let response = LoopsResponse {
8023 function: "no_loops_func".to_string(),
8024 loop_count: 0,
8025 loops: vec![],
8026 };
8027
8028 let wrapper = JsonResponse::new(response);
8029 let json = wrapper.to_json();
8030
8031 assert!(json.contains("\"loop_count\":0"));
8033 assert!(json.contains("\"loops\":[]"));
8034 }
8035
8036 #[test]
8038 fn test_patterns_if_else_detection() {
8039 use crate::cfg::{detect_if_else_patterns, detect_match_patterns};
8040
8041 let cfg = cmds::create_test_cfg();
8042
8043 let if_else_patterns = detect_if_else_patterns(&cfg);
8045 let match_patterns = detect_match_patterns(&cfg);
8046
8047 assert!(
8050 !if_else_patterns.is_empty(),
8051 "Should detect if/else pattern"
8052 );
8053
8054 let pattern = &if_else_patterns[0];
8056 assert_eq!(cfg[pattern.condition].id, 1);
8057 assert_eq!(cfg[pattern.true_branch].id, 2);
8058 assert_eq!(cfg[pattern.false_branch].id, 3);
8059
8060 assert!(
8062 match_patterns.is_empty(),
8063 "Should not detect match patterns in simple if/else"
8064 );
8065 }
8066
8067 #[test]
8069 fn test_patterns_if_else_filter() {
8070 let args = PatternsArgs {
8072 function: "test_func".to_string(),
8073 file: None,
8074 if_else: true,
8075 r#match: false,
8076 };
8077
8078 assert!(args.if_else);
8080 assert!(!args.r#match);
8081 assert_eq!(args.function, "test_func");
8082 }
8083
8084 #[test]
8086 fn test_patterns_match_filter() {
8087 let args = PatternsArgs {
8089 function: "test_func".to_string(),
8090 file: None,
8091 if_else: false,
8092 r#match: true,
8093 };
8094
8095 assert!(!args.if_else);
8097 assert!(args.r#match);
8098 assert_eq!(args.function, "test_func");
8099 }
8100
8101 #[test]
8103 fn test_patterns_json_output() {
8104 let args = PatternsArgs {
8106 function: "test_func".to_string(),
8107 file: None,
8108 if_else: false,
8109 r#match: false,
8110 };
8111
8112 let cli = Cli {
8113 db: None,
8114 output: OutputFormat::Json,
8115 command: Some(Commands::Patterns(args.clone())),
8116 detect_backend: false,
8117 };
8118
8119 assert!(matches!(cli.output, OutputFormat::Json));
8121 }
8122
8123 #[test]
8125 fn test_patterns_response_serialization() {
8126 let response = PatternsResponse {
8127 function: "test_func".to_string(),
8128 if_else_count: 1,
8129 match_count: 0,
8130 if_else_patterns: vec![IfElseInfo {
8131 condition_block: 1,
8132 true_branch: 2,
8133 false_branch: 3,
8134 merge_point: Some(4),
8135 has_else: true,
8136 }],
8137 match_patterns: vec![],
8138 };
8139
8140 let json = serde_json::to_string(&response).unwrap();
8142 assert!(json.contains("\"function\""));
8143 assert!(json.contains("\"if_else_count\""));
8144 assert!(json.contains("\"match_count\""));
8145
8146 assert!(json.contains("\"if_else_patterns\""));
8148 assert!(json.contains("\"condition_block\""));
8149 assert!(json.contains("\"merge_point\""));
8150 }
8151}
8152
8153#[cfg(test)]
8158mod frontiers_tests {
8159 use super::*;
8160 use crate::cfg::{compute_dominance_frontiers, DominatorTree};
8161 use tempfile::NamedTempFile;
8162
8163 fn create_minimal_db() -> anyhow::Result<NamedTempFile> {
8165 use crate::storage::{
8166 REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
8167 };
8168 let file = NamedTempFile::new()?;
8169 let conn = rusqlite::Connection::open(file.path())?;
8170
8171 conn.execute(
8173 "CREATE TABLE magellan_meta (
8174 id INTEGER PRIMARY KEY CHECK (id = 1),
8175 magellan_schema_version INTEGER NOT NULL,
8176 sqlitegraph_schema_version INTEGER NOT NULL,
8177 created_at INTEGER NOT NULL
8178 )",
8179 [],
8180 )?;
8181
8182 conn.execute(
8183 "CREATE TABLE graph_entities (
8184 id INTEGER PRIMARY KEY,
8185 type TEXT NOT NULL,
8186 name TEXT,
8187 source_file TEXT
8188 )",
8189 [],
8190 )?;
8191
8192 conn.execute(
8193 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
8194 VALUES (1, ?, ?, strftime('%s', 'now'))",
8195 [REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION],
8196 )?;
8197
8198 Ok(file)
8199 }
8200
8201 #[test]
8203 fn test_frontiers_response_serialization() {
8204 use crate::output::JsonResponse;
8205
8206 let response = FrontiersResponse {
8207 function: "test_func".to_string(),
8208 nodes_with_frontiers: 2,
8209 frontiers: vec![
8210 NodeFrontier {
8211 node: 1,
8212 frontier_set: vec![3],
8213 },
8214 NodeFrontier {
8215 node: 2,
8216 frontier_set: vec![3],
8217 },
8218 ],
8219 };
8220
8221 let wrapper = JsonResponse::new(response);
8222 let json = wrapper.to_json();
8223
8224 assert!(json.contains("\"function\":\"test_func\""));
8226 assert!(json.contains("\"nodes_with_frontiers\":2"));
8227 assert!(json.contains("\"frontiers\":["));
8228 }
8229
8230 #[test]
8232 fn test_iterated_frontier_response_serialization() {
8233 use crate::output::JsonResponse;
8234
8235 let response = IteratedFrontierResponse {
8236 function: "test_func".to_string(),
8237 iterated_frontier: vec![3, 4],
8238 };
8239
8240 let wrapper = JsonResponse::new(response);
8241 let json = wrapper.to_json();
8242
8243 assert!(json.contains("\"function\":\"test_func\""));
8245 assert!(json.contains("\"iterated_frontier\":[3,4]"));
8246 }
8247
8248 #[test]
8250 fn test_frontiers_basic() {
8251 use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
8252 use petgraph::graph::DiGraph;
8253
8254 let mut g = DiGraph::new();
8256
8257 let b0 = g.add_node(BasicBlock {
8258 id: 0,
8259 db_id: None,
8260 kind: BlockKind::Entry,
8261 statements: vec![],
8262 terminator: Terminator::SwitchInt {
8263 targets: vec![1],
8264 otherwise: 2,
8265 },
8266 source_location: None,
8267 coord_x: 0,
8268 coord_y: 0,
8269 coord_z: 0,
8270 });
8271
8272 let b1 = g.add_node(BasicBlock {
8273 id: 1,
8274 db_id: None,
8275 kind: BlockKind::Normal,
8276 statements: vec!["branch 1".to_string()],
8277 terminator: Terminator::Goto { target: 3 },
8278 source_location: None,
8279 coord_x: 0,
8280 coord_y: 0,
8281 coord_z: 0,
8282 });
8283
8284 let b2 = g.add_node(BasicBlock {
8285 id: 2,
8286 db_id: None,
8287 kind: BlockKind::Normal,
8288 statements: vec!["branch 2".to_string()],
8289 terminator: Terminator::Goto { target: 3 },
8290 source_location: None,
8291 coord_x: 0,
8292 coord_y: 0,
8293 coord_z: 0,
8294 });
8295
8296 let b3 = g.add_node(BasicBlock {
8297 id: 3,
8298 db_id: None,
8299 kind: BlockKind::Exit,
8300 statements: vec![],
8301 terminator: Terminator::Return,
8302 source_location: None,
8303 coord_x: 0,
8304 coord_y: 0,
8305 coord_z: 0,
8306 });
8307
8308 g.add_edge(b0, b1, EdgeType::TrueBranch);
8309 g.add_edge(b0, b2, EdgeType::FalseBranch);
8310 g.add_edge(b1, b3, EdgeType::Fallthrough);
8311 g.add_edge(b2, b3, EdgeType::Fallthrough);
8312
8313 let dom_tree = DominatorTree::new(&g).expect("CFG has entry");
8315 let frontiers = compute_dominance_frontiers(&g, dom_tree);
8316
8317 let df1 = frontiers.frontier(b1);
8321 assert!(df1.contains(&b3));
8322 assert_eq!(df1.len(), 1);
8323
8324 let df2 = frontiers.frontier(b2);
8325 assert!(df2.contains(&b3));
8326 assert_eq!(df2.len(), 1);
8327
8328 let df0 = frontiers.frontier(b0);
8330 assert!(df0.is_empty());
8331 }
8332
8333 #[test]
8335 fn test_frontiers_iterated_flag() {
8336 let args = FrontiersArgs {
8337 function: "test_func".to_string(),
8338 file: None,
8339 iterated: true,
8340 node: None,
8341 };
8342
8343 assert!(args.iterated);
8344 assert!(args.node.is_none());
8345 }
8346
8347 #[test]
8349 fn test_frontiers_node_flag() {
8350 let args = FrontiersArgs {
8351 function: "test_func".to_string(),
8352 file: None,
8353 iterated: false,
8354 node: Some(5),
8355 };
8356
8357 assert!(!args.iterated);
8358 assert_eq!(args.node, Some(5));
8359 }
8360
8361 #[test]
8363 fn test_frontiers_linear_cfg() {
8364 use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
8365 use petgraph::graph::DiGraph;
8366
8367 let mut g = DiGraph::new();
8369
8370 let b0 = g.add_node(BasicBlock {
8371 id: 0,
8372 db_id: None,
8373 kind: BlockKind::Entry,
8374 statements: vec![],
8375 terminator: Terminator::Goto { target: 1 },
8376 source_location: None,
8377 coord_x: 0,
8378 coord_y: 0,
8379 coord_z: 0,
8380 });
8381
8382 let b1 = g.add_node(BasicBlock {
8383 id: 1,
8384 db_id: None,
8385 kind: BlockKind::Normal,
8386 statements: vec![],
8387 terminator: Terminator::Goto { target: 2 },
8388 source_location: None,
8389 coord_x: 0,
8390 coord_y: 0,
8391 coord_z: 0,
8392 });
8393
8394 let b2 = g.add_node(BasicBlock {
8395 id: 2,
8396 db_id: None,
8397 kind: BlockKind::Normal,
8398 statements: vec![],
8399 terminator: Terminator::Goto { target: 3 },
8400 source_location: None,
8401 coord_x: 0,
8402 coord_y: 0,
8403 coord_z: 0,
8404 });
8405
8406 let b3 = g.add_node(BasicBlock {
8407 id: 3,
8408 db_id: None,
8409 kind: BlockKind::Exit,
8410 statements: vec![],
8411 terminator: Terminator::Return,
8412 source_location: None,
8413 coord_x: 0,
8414 coord_y: 0,
8415 coord_z: 0,
8416 });
8417
8418 g.add_edge(b0, b1, EdgeType::Fallthrough);
8419 g.add_edge(b1, b2, EdgeType::Fallthrough);
8420 g.add_edge(b2, b3, EdgeType::Fallthrough);
8421
8422 let dom_tree = DominatorTree::new(&g).expect("CFG has entry");
8424 let frontiers = compute_dominance_frontiers(&g, dom_tree);
8425
8426 let nodes_with_frontiers: Vec<_> = frontiers.nodes_with_frontiers().collect();
8428 assert!(nodes_with_frontiers.is_empty());
8429 }
8430
8431 #[test]
8433 fn test_frontiers_loop_cfg() {
8434 use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
8435 use petgraph::graph::DiGraph;
8436
8437 let mut g = DiGraph::new();
8439
8440 let b0 = g.add_node(BasicBlock {
8441 id: 0,
8442 db_id: None,
8443 kind: BlockKind::Entry,
8444 statements: vec![],
8445 terminator: Terminator::Goto { target: 1 },
8446 source_location: None,
8447 coord_x: 0,
8448 coord_y: 0,
8449 coord_z: 0,
8450 });
8451
8452 let b1 = g.add_node(BasicBlock {
8453 id: 1,
8454 db_id: None,
8455 kind: BlockKind::Normal,
8456 statements: vec![],
8457 terminator: Terminator::SwitchInt {
8458 targets: vec![2],
8459 otherwise: 3,
8460 },
8461 source_location: None,
8462 coord_x: 0,
8463 coord_y: 0,
8464 coord_z: 0,
8465 });
8466
8467 let b2 = g.add_node(BasicBlock {
8468 id: 2,
8469 db_id: None,
8470 kind: BlockKind::Normal,
8471 statements: vec!["loop body".to_string()],
8472 terminator: Terminator::Goto { target: 1 },
8473 source_location: None,
8474 coord_x: 0,
8475 coord_y: 0,
8476 coord_z: 0,
8477 });
8478
8479 let b3 = g.add_node(BasicBlock {
8480 id: 3,
8481 db_id: None,
8482 kind: BlockKind::Exit,
8483 statements: vec![],
8484 terminator: Terminator::Return,
8485 source_location: None,
8486 coord_x: 0,
8487 coord_y: 0,
8488 coord_z: 0,
8489 });
8490
8491 g.add_edge(b0, b1, EdgeType::Fallthrough);
8492 g.add_edge(b1, b2, EdgeType::TrueBranch);
8493 g.add_edge(b1, b3, EdgeType::FalseBranch);
8494 g.add_edge(b2, b1, EdgeType::LoopBack);
8495
8496 let dom_tree = DominatorTree::new(&g).expect("CFG has entry");
8498 let frontiers = compute_dominance_frontiers(&g, dom_tree);
8499
8500 let df1 = frontiers.frontier(b1);
8502 assert!(df1.contains(&b1), "Loop header should have self-frontier");
8503 }
8504
8505 #[test]
8507 fn test_frontiers_json_output_format() {
8508 use crate::output::JsonResponse;
8509
8510 let response = FrontiersResponse {
8511 function: "json_test".to_string(),
8512 nodes_with_frontiers: 2,
8513 frontiers: vec![
8514 NodeFrontier {
8515 node: 1,
8516 frontier_set: vec![3],
8517 },
8518 NodeFrontier {
8519 node: 2,
8520 frontier_set: vec![3],
8521 },
8522 ],
8523 };
8524
8525 let wrapper = JsonResponse::new(response);
8526 let json = wrapper.to_json();
8527
8528 assert!(json.contains("\"schema_version\""));
8530 assert!(json.contains("\"execution_id\""));
8531 assert!(json.contains("\"tool\""));
8532 assert!(json.contains("\"timestamp\""));
8533 assert!(json.contains("\"data\""));
8534 }
8535
8536 #[test]
8538 fn test_frontiers_response_empty() {
8539 use crate::output::JsonResponse;
8540
8541 let response = FrontiersResponse {
8542 function: "linear_func".to_string(),
8543 nodes_with_frontiers: 0,
8544 frontiers: vec![],
8545 };
8546
8547 let wrapper = JsonResponse::new(response);
8548 let json = wrapper.to_json();
8549
8550 assert!(json.contains("\"nodes_with_frontiers\":0"));
8552 assert!(json.contains("\"frontiers\":[]"));
8553 }
8554
8555 #[test]
8561 fn test_hotspots_args_parsing() {
8562 let args = HotspotsArgs {
8563 entry: "main".to_string(),
8564 top: 10,
8565 min_paths: Some(5),
8566 verbose: true,
8567 inter_procedural: false,
8568 intra_procedural: false,
8569 };
8570
8571 assert_eq!(args.entry, "main");
8572 assert_eq!(args.top, 10);
8573 assert_eq!(args.min_paths, Some(5));
8574 assert!(args.verbose);
8575 assert!(!args.inter_procedural);
8576 }
8577
8578 #[test]
8580 fn test_hotspots_args_default_entry() {
8581 let args = HotspotsArgs {
8582 entry: "main".to_string(), top: 20,
8584 min_paths: None,
8585 verbose: false,
8586 inter_procedural: false,
8587 intra_procedural: false,
8588 };
8589
8590 assert_eq!(args.entry, "main");
8591 assert_eq!(args.top, 20); }
8593
8594 #[test]
8596 fn test_hotspot_entry_serialization() {
8597 let entry = HotspotEntry {
8598 function: "test_func".to_string(),
8599 risk_score: 42.5,
8600 path_count: 10,
8601 dominance_factor: 1.5,
8602 complexity: 5,
8603 file_path: "test.rs".to_string(),
8604 };
8605
8606 let json = serde_json::to_string(&entry).unwrap();
8607 assert!(json.contains("test_func"));
8608 assert!(json.contains("42.5"));
8609 assert!(json.contains("\"path_count\":10"));
8610 }
8611
8612 #[test]
8614 fn test_hotspots_response_serialization() {
8615 use crate::output::JsonResponse;
8616
8617 let response = HotspotsResponse {
8618 entry_point: "main".to_string(),
8619 total_functions: 100,
8620 hotspots: vec![],
8621 mode: "intra-procedural".to_string(),
8622 };
8623
8624 let wrapper = JsonResponse::new(response);
8625 let json = wrapper.to_json();
8626
8627 assert!(json.contains("\"entry_point\":\"main\""));
8628 assert!(json.contains("\"total_functions\":100"));
8629 assert!(json.contains("intra-procedural"));
8630 }
8631
8632 #[test]
8634 fn test_hotspots_response_with_entries() {
8635 use crate::output::JsonResponse;
8636
8637 let hotspot = HotspotEntry {
8638 function: "risky_func".to_string(),
8639 risk_score: 85.0,
8640 path_count: 50,
8641 dominance_factor: 3.0,
8642 complexity: 15,
8643 file_path: "src/lib.rs".to_string(),
8644 };
8645
8646 let response = HotspotsResponse {
8647 entry_point: "main".to_string(),
8648 total_functions: 10,
8649 hotspots: vec![hotspot],
8650 mode: "inter-procedural".to_string(),
8651 };
8652
8653 let wrapper = JsonResponse::new(response);
8654 let json = wrapper.to_json();
8655
8656 assert!(json.contains("risky_func"));
8657 assert!(json.contains("85"));
8658 assert!(json.contains("inter-procedural"));
8659 }
8660
8661 #[test]
8663 fn test_hotspot_entry_clone() {
8664 let entry = HotspotEntry {
8665 function: "func".to_string(),
8666 risk_score: 1.0,
8667 path_count: 1,
8668 dominance_factor: 1.0,
8669 complexity: 1,
8670 file_path: "file.rs".to_string(),
8671 };
8672
8673 let cloned = entry.clone();
8674 assert_eq!(entry.function, cloned.function);
8675 assert_eq!(entry.risk_score, cloned.risk_score);
8676 }
8677
8678 #[test]
8684 fn test_hotpaths_args_parsing() {
8685 let args = HotpathsArgs {
8686 function: "my_function".to_string(),
8687 top: 5,
8688 rationale: true,
8689 min_score: Some(0.5),
8690 };
8691
8692 assert_eq!(args.function, "my_function");
8693 assert_eq!(args.top, 5);
8694 assert!(args.rationale);
8695 assert_eq!(args.min_score, Some(0.5));
8696 }
8697
8698 #[test]
8700 fn test_hotpaths_args_defaults() {
8701 let args = HotpathsArgs {
8702 function: "main".to_string(),
8703 top: 10, rationale: false,
8705 min_score: None,
8706 };
8707
8708 assert_eq!(args.function, "main");
8709 assert_eq!(args.top, 10); assert!(!args.rationale);
8711 assert!(args.min_score.is_none());
8712 }
8713
8714 #[test]
8720 fn test_dominators_args_has_inter_procedural_flag() {
8721 let args = DominatorsArgs {
8722 function: "main".to_string(),
8723 file: None,
8724 must_pass_through: Some("block1".to_string()),
8725 post: false,
8726 inter_procedural: true,
8727 };
8728
8729 assert!(args.inter_procedural);
8730 assert_eq!(args.function, "main");
8731 assert_eq!(args.must_pass_through, Some("block1".to_string()));
8732 assert!(!args.post);
8733 }
8734
8735 #[test]
8737 fn test_dominators_args_default_intra_procedural() {
8738 let args = DominatorsArgs {
8739 function: "main".to_string(),
8740 file: None,
8741 must_pass_through: None,
8742 post: false,
8743 inter_procedural: false, };
8745
8746 assert!(!args.inter_procedural);
8747 assert!(!args.post);
8748 assert!(args.must_pass_through.is_none());
8749 }
8750
8751 #[test]
8753 fn test_dominators_inter_procedural_with_post() {
8754 let args = DominatorsArgs {
8756 function: "entry".to_string(),
8757 file: None,
8758 must_pass_through: None,
8759 post: true,
8760 inter_procedural: true,
8761 };
8762
8763 assert!(args.inter_procedural);
8765 assert!(args.post);
8766 }
8767
8768 #[test]
8770 fn test_dominators_inter_procedural_must_pass_through_combination() {
8771 let args = DominatorsArgs {
8773 function: "main".to_string(),
8774 file: None,
8775 must_pass_through: Some("some_block".to_string()),
8776 post: false,
8777 inter_procedural: true,
8778 };
8779
8780 assert!(args.inter_procedural);
8781 assert!(args.must_pass_through.is_some());
8782 }
8784}