1use super::{
13 DeadCodeResult, FlowResult, ImpactResult, ModuleResult, PatternResult, RefsResult, ScopeResult,
14 StatsResult, TraceFormatter, TraceResult,
15};
16
17pub struct PlainFormatter;
23
24impl PlainFormatter {
25 pub fn new() -> Self {
27 Self
28 }
29}
30
31impl Default for PlainFormatter {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl TraceFormatter for PlainFormatter {
38 fn format_trace(&self, result: &TraceResult) -> String {
39 let mut output = String::new();
40
41 output.push_str(&format!("TRACE: {}\n", result.symbol));
42 if let Some(ref defined_at) = result.defined_at {
43 output.push_str(&format!("Defined: {}\n", defined_at));
44 }
45 output.push_str(&format!(
46 "Found: {} invocation paths from {} entry points\n",
47 result.total_paths, result.entry_points
48 ));
49 output.push_str(&"-".repeat(60));
50 output.push('\n');
51
52 for (i, path) in result.invocation_paths.iter().enumerate() {
53 output.push_str(&format!(
54 "\nPath {}/{} (entry: {})\n",
55 i + 1,
56 result.total_paths,
57 path.entry_point
58 ));
59
60 for (j, step) in path.chain.iter().enumerate() {
61 let prefix = if j == path.chain.len() - 1 {
62 " -> "
63 } else {
64 " "
65 };
66 output.push_str(&format!(
67 "{}{}:{} - {}\n",
68 prefix, step.file, step.line, step.symbol
69 ));
70
71 if let Some(ref ctx) = step.context {
73 for line in ctx.lines() {
74 output.push_str(&format!(" {}\n", line));
75 }
76 }
77 }
78 }
79
80 output
81 }
82
83 fn format_refs(&self, result: &RefsResult) -> String {
84 let mut output = String::new();
85
86 output.push_str(&format!("REFS: {}\n", result.symbol));
87 if let Some(ref defined_at) = result.defined_at {
88 output.push_str(&format!("Defined: {}\n", defined_at));
89 }
90 output.push_str(&format!("Found: {} references\n", result.total_refs));
91
92 if !result.by_kind.is_empty() {
93 output.push_str("By kind: ");
94 let kinds: Vec<_> = result
95 .by_kind
96 .iter()
97 .map(|(k, v)| format!("{}={}", k, v))
98 .collect();
99 output.push_str(&kinds.join(", "));
100 output.push('\n');
101 }
102 output.push_str(&"-".repeat(60));
103 output.push('\n');
104
105 let mut by_file: std::collections::HashMap<&str, Vec<_>> = std::collections::HashMap::new();
106 for r in &result.references {
107 by_file.entry(&r.file).or_default().push(r);
108 }
109
110 for (file, refs) in by_file {
111 output.push_str(&format!("\n{}:\n", file));
112 for r in refs {
113 let context_lines: Vec<&str> = r.context.lines().collect();
115 if context_lines.len() > 1 {
116 output.push_str(&format!(" {}:{} [{}]\n", r.line, r.column, r.kind));
117 for line in &context_lines {
118 output.push_str(&format!(" {}\n", line));
119 }
120 } else {
121 output.push_str(&format!(
122 " {}:{} [{}] {}",
123 r.line,
124 r.column,
125 r.kind,
126 r.context.trim()
127 ));
128 if let Some(ref enclosing) = r.enclosing_symbol {
129 output.push_str(&format!(" (in {})", enclosing));
130 }
131 output.push('\n');
132 }
133 }
134 }
135
136 output
137 }
138
139 fn format_dead_code(&self, result: &DeadCodeResult) -> String {
140 let mut output = String::new();
141
142 output.push_str("DEAD CODE ANALYSIS\n");
143 output.push_str(&format!("Found: {} unused symbols\n", result.total_dead));
144
145 if !result.by_kind.is_empty() {
146 output.push_str("By kind: ");
147 let kinds: Vec<_> = result
148 .by_kind
149 .iter()
150 .map(|(k, v)| format!("{}={}", k, v))
151 .collect();
152 output.push_str(&kinds.join(", "));
153 output.push('\n');
154 }
155 output.push_str(&"-".repeat(60));
156 output.push('\n');
157
158 for sym in &result.symbols {
159 output.push_str(&format!(
160 "{} {}:{} {} - {}\n",
161 sym.kind, sym.file, sym.line, sym.name, sym.reason
162 ));
163 }
164
165 output
166 }
167
168 fn format_flow(&self, result: &FlowResult) -> String {
169 let mut output = String::new();
170
171 output.push_str(&format!("DATA FLOW: {}\n", result.symbol));
172 output.push_str(&format!("Paths: {}\n", result.flow_paths.len()));
173 output.push_str(&"-".repeat(60));
174 output.push('\n');
175
176 for (i, path) in result.flow_paths.iter().enumerate() {
177 output.push_str(&format!("\nFlow Path {}:\n", i + 1));
178
179 for step in path {
180 output.push_str(&format!(
181 " {}:{} [{}] {}\n",
182 step.file,
183 step.line,
184 step.action,
185 step.expression.trim()
186 ));
187 }
188 }
189
190 output
191 }
192
193 fn format_impact(&self, result: &ImpactResult) -> String {
194 let mut output = String::new();
195
196 output.push_str(&format!("IMPACT ANALYSIS: {}\n", result.symbol));
197 output.push_str(&format!("File: {}\n", result.file));
198 output.push_str(&format!("Risk Level: {}\n", result.risk_level));
199 output.push_str(&"-".repeat(60));
200 output.push('\n');
201
202 output.push_str(&format!(
203 "\nDirect callers ({}):\n",
204 result.direct_caller_count
205 ));
206 for caller in &result.direct_callers {
207 output.push_str(&format!(" {}\n", caller));
208 }
209
210 if !result.transitive_callers.is_empty() {
211 output.push_str(&format!(
212 "\nTransitive callers ({}):\n",
213 result.transitive_caller_count
214 ));
215 for caller in &result.transitive_callers {
216 output.push_str(&format!(" {}\n", caller));
217 }
218 }
219
220 output.push_str(&format!(
221 "\nAffected entry points ({}):\n",
222 result.affected_entry_points.len()
223 ));
224 for ep in &result.affected_entry_points {
225 output.push_str(&format!(" {}\n", ep));
226 }
227
228 output.push_str(&format!(
229 "\nFiles affected: {}\n",
230 result.files_affected.len()
231 ));
232
233 output
234 }
235
236 fn format_module(&self, result: &ModuleResult) -> String {
237 let mut output = String::new();
238
239 output.push_str(&format!("MODULE: {}\n", result.module));
240 output.push_str(&format!("Path: {}\n", result.file_path));
241 output.push_str(&"-".repeat(60));
242 output.push('\n');
243
244 if !result.exports.is_empty() {
245 output.push_str(&format!("\nExports ({}):\n", result.exports.len()));
246 for export in &result.exports {
247 output.push_str(&format!(" {}\n", export));
248 }
249 }
250
251 if !result.imported_by.is_empty() {
252 output.push_str(&format!("\nImported by ({}):\n", result.imported_by.len()));
253 for importer in &result.imported_by {
254 output.push_str(&format!(" {}\n", importer));
255 }
256 }
257
258 if !result.dependencies.is_empty() {
259 output.push_str(&format!(
260 "\nDependencies ({}):\n",
261 result.dependencies.len()
262 ));
263 for dep in &result.dependencies {
264 output.push_str(&format!(" {}\n", dep));
265 }
266 }
267
268 if !result.circular_deps.is_empty() {
269 output.push_str(&format!(
270 "\nCIRCULAR DEPENDENCIES ({}):\n",
271 result.circular_deps.len()
272 ));
273 for cycle in &result.circular_deps {
274 output.push_str(&format!(" WARNING: {}\n", cycle));
275 }
276 }
277
278 output
279 }
280
281 fn format_pattern(&self, result: &PatternResult) -> String {
282 let mut output = String::new();
283
284 output.push_str(&format!("PATTERN: {}\n", result.pattern));
285 output.push_str(&format!(
286 "Found: {} matches in {} files\n",
287 result.total_matches,
288 result.by_file.len()
289 ));
290 output.push_str(&"-".repeat(60));
291 output.push('\n');
292
293 let mut by_file: std::collections::HashMap<&str, Vec<_>> = std::collections::HashMap::new();
294 for m in &result.matches {
295 by_file.entry(&m.file).or_default().push(m);
296 }
297
298 for (file, matches) in by_file {
299 output.push_str(&format!("\n{}:\n", file));
300 for m in matches {
301 let context_lines: Vec<&str> = m.context.lines().collect();
303 if context_lines.len() > 1 {
304 output.push_str(&format!(" {}:{}:\n", m.line, m.column));
305 for line in &context_lines {
306 output.push_str(&format!(" {}\n", line));
307 }
308 } else {
309 output.push_str(&format!(
310 " {}:{}: {}\n",
311 m.line,
312 m.column,
313 m.context.trim()
314 ));
315 }
316 }
317 }
318
319 output
320 }
321
322 fn format_scope(&self, result: &ScopeResult) -> String {
323 let mut output = String::new();
324
325 output.push_str(&format!("SCOPE AT: {}:{}\n", result.file, result.line));
326 if let Some(ref scope) = result.enclosing_scope {
327 output.push_str(&format!("Enclosing: {}\n", scope));
328 }
329 output.push_str(&"-".repeat(60));
330 output.push('\n');
331
332 if !result.local_variables.is_empty() {
333 output.push_str(&format!(
334 "\nLocal Variables ({}):\n",
335 result.local_variables.len()
336 ));
337 for var in &result.local_variables {
338 output.push_str(&format!(
339 " {}: {} (line {})\n",
340 var.name, var.kind, var.defined_at
341 ));
342 }
343 }
344
345 if !result.parameters.is_empty() {
346 output.push_str(&format!("\nParameters ({}):\n", result.parameters.len()));
347 for param in &result.parameters {
348 output.push_str(&format!(" {}: {}\n", param.name, param.kind));
349 }
350 }
351
352 if !result.imports.is_empty() {
353 output.push_str(&format!("\nImports ({}):\n", result.imports.len()));
354 for import in &result.imports {
355 output.push_str(&format!(" {}\n", import));
356 }
357 }
358
359 output
360 }
361
362 fn format_stats(&self, result: &StatsResult) -> String {
363 let mut output = String::new();
364
365 output.push_str("CODEBASE STATISTICS\n");
366 output.push_str(&"-".repeat(60));
367 output.push('\n');
368
369 output.push_str(&format!("\nFiles: {}\n", result.total_files));
370 output.push_str(&format!("Symbols: {}\n", result.total_symbols));
371 output.push_str(&format!("Tokens: {}\n", result.total_tokens));
372 output.push_str(&format!("References: {}\n", result.total_references));
373 output.push_str(&format!("Call Edges: {}\n", result.total_edges));
374 output.push_str(&format!("Entry Points: {}\n", result.total_entry_points));
375
376 output.push_str("\nSymbols by kind:\n");
377 let mut kinds: Vec<_> = result.symbols_by_kind.iter().collect();
378 kinds.sort_by(|a, b| b.1.cmp(a.1));
379 for (kind, count) in &kinds {
380 output.push_str(&format!(" {}: {}\n", kind, count));
381 }
382
383 output.push_str("\nCall Graph:\n");
384 output.push_str(&format!(" Max Depth: {}\n", result.max_call_depth));
385 output.push_str(&format!(" Avg Depth: {:.1}\n", result.avg_call_depth));
386
387 output
388 }
389}
390
391pub struct CsvFormatter;
397
398impl CsvFormatter {
399 pub fn new() -> Self {
400 Self
401 }
402
403 fn escape_csv(s: &str) -> String {
404 if s.contains(',') || s.contains('"') || s.contains('\n') {
405 format!("\"{}\"", s.replace('"', "\"\""))
406 } else {
407 s.to_string()
408 }
409 }
410}
411
412impl Default for CsvFormatter {
413 fn default() -> Self {
414 Self::new()
415 }
416}
417
418impl TraceFormatter for CsvFormatter {
419 fn format_trace(&self, result: &TraceResult) -> String {
420 let mut output = String::from("path_num,entry_point,entry_kind,step,symbol,file,line\n");
421
422 for (i, path) in result.invocation_paths.iter().enumerate() {
423 for (j, step) in path.chain.iter().enumerate() {
424 output.push_str(&format!(
425 "{},{},{},{},{},{},{}\n",
426 i + 1,
427 Self::escape_csv(&path.entry_point),
428 Self::escape_csv(&path.entry_kind),
429 j + 1,
430 Self::escape_csv(&step.symbol),
431 Self::escape_csv(&step.file),
432 step.line
433 ));
434 }
435 }
436
437 output
438 }
439
440 fn format_refs(&self, result: &RefsResult) -> String {
441 let mut output = String::from("file,line,column,kind,context,enclosing_symbol\n");
442
443 for r in &result.references {
444 let context_single = r.context.lines().next().unwrap_or("").trim();
445 output.push_str(&format!(
446 "{},{},{},{},{},{}\n",
447 Self::escape_csv(&r.file),
448 r.line,
449 r.column,
450 r.kind,
451 Self::escape_csv(context_single),
452 Self::escape_csv(r.enclosing_symbol.as_deref().unwrap_or(""))
453 ));
454 }
455
456 output
457 }
458
459 fn format_dead_code(&self, result: &DeadCodeResult) -> String {
460 let mut output = String::from("name,kind,file,line,reason\n");
461
462 for sym in &result.symbols {
463 output.push_str(&format!(
464 "{},{},{},{},{}\n",
465 Self::escape_csv(&sym.name),
466 Self::escape_csv(&sym.kind),
467 Self::escape_csv(&sym.file),
468 sym.line,
469 Self::escape_csv(&sym.reason)
470 ));
471 }
472
473 output
474 }
475
476 fn format_flow(&self, result: &FlowResult) -> String {
477 let mut output = String::from("path,step,variable,action,file,line,expression\n");
478
479 for (i, path) in result.flow_paths.iter().enumerate() {
480 for (j, step) in path.iter().enumerate() {
481 output.push_str(&format!(
482 "{},{},{},{},{},{},{}\n",
483 i + 1,
484 j + 1,
485 Self::escape_csv(&step.variable),
486 step.action,
487 Self::escape_csv(&step.file),
488 step.line,
489 Self::escape_csv(step.expression.trim())
490 ));
491 }
492 }
493
494 output
495 }
496
497 fn format_impact(&self, result: &ImpactResult) -> String {
498 let mut output = String::from("type,value\n");
499
500 output.push_str(&format!("symbol,{}\n", Self::escape_csv(&result.symbol)));
501 output.push_str(&format!("file,{}\n", Self::escape_csv(&result.file)));
502 output.push_str(&format!("risk_level,{}\n", result.risk_level));
503 output.push_str(&format!(
504 "direct_caller_count,{}\n",
505 result.direct_caller_count
506 ));
507 output.push_str(&format!(
508 "transitive_caller_count,{}\n",
509 result.transitive_caller_count
510 ));
511 output.push_str(&format!(
512 "affected_entry_points,{}\n",
513 result.affected_entry_points.len()
514 ));
515 output.push_str(&format!("files_affected,{}\n", result.files_affected.len()));
516
517 output
518 }
519
520 fn format_module(&self, result: &ModuleResult) -> String {
521 let mut output = String::from("type,value\n");
522
523 output.push_str(&format!("module,{}\n", Self::escape_csv(&result.module)));
524 output.push_str(&format!("path,{}\n", Self::escape_csv(&result.file_path)));
525 output.push_str(&format!("exports,{}\n", result.exports.len()));
526 output.push_str(&format!("imported_by,{}\n", result.imported_by.len()));
527 output.push_str(&format!("dependencies,{}\n", result.dependencies.len()));
528 output.push_str(&format!("circular_deps,{}\n", result.circular_deps.len()));
529
530 output
531 }
532
533 fn format_pattern(&self, result: &PatternResult) -> String {
534 let mut output = String::from("file,line,column,matched_text,context\n");
535
536 for m in &result.matches {
537 let context_single = m.context.lines().next().unwrap_or("").trim();
538 output.push_str(&format!(
539 "{},{},{},{},{}\n",
540 Self::escape_csv(&m.file),
541 m.line,
542 m.column,
543 Self::escape_csv(&m.matched_text),
544 Self::escape_csv(context_single)
545 ));
546 }
547
548 output
549 }
550
551 fn format_scope(&self, result: &ScopeResult) -> String {
552 let mut output = String::from("type,name,kind,defined_at\n");
553
554 for var in &result.local_variables {
555 output.push_str(&format!(
556 "local,{},{},{}\n",
557 Self::escape_csv(&var.name),
558 Self::escape_csv(&var.kind),
559 var.defined_at
560 ));
561 }
562
563 for param in &result.parameters {
564 output.push_str(&format!(
565 "param,{},{},\n",
566 Self::escape_csv(¶m.name),
567 Self::escape_csv(¶m.kind)
568 ));
569 }
570
571 for import in &result.imports {
572 output.push_str(&format!("import,{},module,\n", Self::escape_csv(import)));
573 }
574
575 output
576 }
577
578 fn format_stats(&self, result: &StatsResult) -> String {
579 let mut output = String::from("metric,value\n");
580
581 output.push_str(&format!("total_files,{}\n", result.total_files));
582 output.push_str(&format!("total_symbols,{}\n", result.total_symbols));
583 output.push_str(&format!("total_tokens,{}\n", result.total_tokens));
584 output.push_str(&format!("total_references,{}\n", result.total_references));
585 output.push_str(&format!("total_edges,{}\n", result.total_edges));
586 output.push_str(&format!(
587 "total_entry_points,{}\n",
588 result.total_entry_points
589 ));
590 output.push_str(&format!("max_call_depth,{}\n", result.max_call_depth));
591 output.push_str(&format!("avg_call_depth,{:.2}\n", result.avg_call_depth));
592
593 output
594 }
595}
596
597pub struct DotFormatter;
603
604impl DotFormatter {
605 pub fn new() -> Self {
606 Self
607 }
608
609 fn escape_dot(s: &str) -> String {
610 s.replace('"', "\\\"").replace('\n', "\\n")
611 }
612}
613
614impl Default for DotFormatter {
615 fn default() -> Self {
616 Self::new()
617 }
618}
619
620impl TraceFormatter for DotFormatter {
621 fn format_trace(&self, result: &TraceResult) -> String {
622 let mut output = String::from("digraph trace {\n");
623 output.push_str(" rankdir=LR;\n");
624 output.push_str(" node [shape=box];\n\n");
625
626 let mut nodes = std::collections::HashSet::new();
627 let mut edges = Vec::new();
628
629 for path in &result.invocation_paths {
630 for (i, step) in path.chain.iter().enumerate() {
631 let node_id = format!("{}_{}", step.symbol.replace(['.', '/'], "_"), step.line);
632 if !nodes.contains(&node_id) {
633 nodes.insert(node_id.clone());
634 output.push_str(&format!(
635 " {} [label=\"{}\\n{}:{}\"];\n",
636 node_id,
637 Self::escape_dot(&step.symbol),
638 Self::escape_dot(&step.file),
639 step.line
640 ));
641 }
642
643 if i > 0 {
644 let prev = &path.chain[i - 1];
645 let prev_id = format!("{}_{}", prev.symbol.replace(['.', '/'], "_"), prev.line);
646 let edge = (prev_id.clone(), node_id.clone());
647 if !edges.contains(&edge) {
648 edges.push(edge.clone());
649 output.push_str(&format!(" {} -> {};\n", edge.0, edge.1));
650 }
651 }
652 }
653 }
654
655 output.push_str("}\n");
656 output
657 }
658
659 fn format_refs(&self, result: &RefsResult) -> String {
660 let mut output = String::from("digraph refs {\n");
661 output.push_str(" rankdir=TB;\n");
662 output.push_str(&format!(
663 " center [label=\"{}\" shape=ellipse style=filled fillcolor=yellow];\n",
664 Self::escape_dot(&result.symbol)
665 ));
666
667 for (i, r) in result.references.iter().enumerate() {
668 let node_id = format!("ref_{}", i);
669 let label = format!("{}:{}", r.file, r.line);
670 output.push_str(&format!(
671 " {} [label=\"{}\" shape=box];\n",
672 node_id,
673 Self::escape_dot(&label)
674 ));
675 output.push_str(&format!(
676 " center -> {} [label=\"{}\"];\n",
677 node_id, r.kind
678 ));
679 }
680
681 output.push_str("}\n");
682 output
683 }
684
685 fn format_dead_code(&self, result: &DeadCodeResult) -> String {
686 let mut output = String::from("digraph dead_code {\n");
687 output.push_str(" node [shape=box style=filled fillcolor=lightgray];\n");
688
689 for (i, sym) in result.symbols.iter().enumerate() {
690 output.push_str(&format!(
691 " dead_{} [label=\"{}\\n{}:{}\"];\n",
692 i,
693 Self::escape_dot(&sym.name),
694 Self::escape_dot(&sym.file),
695 sym.line
696 ));
697 }
698
699 output.push_str("}\n");
700 output
701 }
702
703 fn format_flow(&self, result: &FlowResult) -> String {
704 let mut output = String::from("digraph flow {\n");
705 output.push_str(" rankdir=TB;\n");
706
707 for (path_idx, path) in result.flow_paths.iter().enumerate() {
708 for (i, step) in path.iter().enumerate() {
709 let node_id = format!("p{}_s{}", path_idx, i);
710 output.push_str(&format!(
711 " {} [label=\"[{}] {}\\n{}:{}\"];\n",
712 node_id,
713 step.action,
714 Self::escape_dot(&step.variable),
715 Self::escape_dot(&step.file),
716 step.line
717 ));
718
719 if i > 0 {
720 let prev_id = format!("p{}_s{}", path_idx, i - 1);
721 output.push_str(&format!(" {} -> {};\n", prev_id, node_id));
722 }
723 }
724 }
725
726 output.push_str("}\n");
727 output
728 }
729
730 fn format_impact(&self, result: &ImpactResult) -> String {
731 let mut output = String::from("digraph impact {\n");
732 output.push_str(" rankdir=BT;\n");
733 output.push_str(&format!(
734 " target [label=\"{}\" shape=ellipse style=filled fillcolor=red fontcolor=white];\n",
735 Self::escape_dot(&result.symbol)
736 ));
737
738 for (i, caller) in result.direct_callers.iter().enumerate() {
739 output.push_str(&format!(
740 " direct_{} [label=\"{}\"];\n",
741 i,
742 Self::escape_dot(caller)
743 ));
744 output.push_str(&format!(" direct_{} -> target [color=red];\n", i));
745 }
746
747 output.push_str("}\n");
748 output
749 }
750
751 fn format_module(&self, result: &ModuleResult) -> String {
752 let mut output = String::from("digraph module {\n");
753 output.push_str(" rankdir=LR;\n");
754 output.push_str(&format!(
755 " module [label=\"{}\" shape=box style=filled fillcolor=lightblue];\n",
756 Self::escape_dot(&result.module)
757 ));
758
759 for (i, dep) in result.dependencies.iter().enumerate() {
760 output.push_str(&format!(
761 " dep_{} [label=\"{}\"];\n",
762 i,
763 Self::escape_dot(dep)
764 ));
765 output.push_str(&format!(" module -> dep_{};\n", i));
766 }
767
768 for (i, importer) in result.imported_by.iter().enumerate() {
769 output.push_str(&format!(
770 " importer_{} [label=\"{}\"];\n",
771 i,
772 Self::escape_dot(importer)
773 ));
774 output.push_str(&format!(" importer_{} -> module;\n", i));
775 }
776
777 output.push_str("}\n");
778 output
779 }
780
781 fn format_pattern(&self, _result: &PatternResult) -> String {
782 String::from("// Pattern results not suitable for DOT format\n")
783 }
784
785 fn format_scope(&self, _result: &ScopeResult) -> String {
786 String::from("// Scope results not suitable for DOT format\n")
787 }
788
789 fn format_stats(&self, _result: &StatsResult) -> String {
790 String::from("// Stats not suitable for DOT format\n")
791 }
792}
793
794pub struct MarkdownFormatter;
800
801impl MarkdownFormatter {
802 pub fn new() -> Self {
803 Self
804 }
805}
806
807impl Default for MarkdownFormatter {
808 fn default() -> Self {
809 Self::new()
810 }
811}
812
813impl TraceFormatter for MarkdownFormatter {
814 fn format_trace(&self, result: &TraceResult) -> String {
815 let mut output = String::new();
816
817 output.push_str(&format!("# Trace: {}\n\n", result.symbol));
818
819 if let Some(ref defined_at) = result.defined_at {
820 output.push_str(&format!("**Defined at:** `{}`\n\n", defined_at));
821 }
822
823 output.push_str(&format!(
824 "**Found:** {} invocation paths from {} entry points\n\n",
825 result.total_paths, result.entry_points
826 ));
827
828 for (i, path) in result.invocation_paths.iter().enumerate() {
829 output.push_str(&format!("## Path {}/{}\n\n", i + 1, result.total_paths));
830 output.push_str(&format!(
831 "**Entry:** {} ({})\n\n",
832 path.entry_point, path.entry_kind
833 ));
834
835 output.push_str("| Step | Symbol | Location |\n");
836 output.push_str("|------|--------|----------|\n");
837
838 for (j, step) in path.chain.iter().enumerate() {
839 output.push_str(&format!(
840 "| {} | `{}` | `{}:{}` |\n",
841 j + 1,
842 step.symbol,
843 step.file,
844 step.line
845 ));
846 }
847 output.push('\n');
848 }
849
850 output
851 }
852
853 fn format_refs(&self, result: &RefsResult) -> String {
854 let mut output = String::new();
855
856 output.push_str(&format!("# References: {}\n\n", result.symbol));
857
858 if let Some(ref defined_at) = result.defined_at {
859 output.push_str(&format!("**Defined at:** `{}`\n\n", defined_at));
860 }
861
862 output.push_str(&format!("**Total:** {} references\n\n", result.total_refs));
863
864 if !result.by_kind.is_empty() {
865 output.push_str("### By Kind\n\n");
866 for (kind, count) in &result.by_kind {
867 output.push_str(&format!("- **{}:** {}\n", kind, count));
868 }
869 output.push('\n');
870 }
871
872 output.push_str("### References\n\n");
873 output.push_str("| File | Line | Kind | Context |\n");
874 output.push_str("|------|------|------|----------|\n");
875
876 for r in &result.references {
877 let context_short = r.context.lines().next().unwrap_or("").trim();
878 let context_escaped = context_short.replace('|', "\\|");
879 output.push_str(&format!(
880 "| `{}` | {} | {} | `{}` |\n",
881 r.file, r.line, r.kind, context_escaped
882 ));
883 }
884
885 output
886 }
887
888 fn format_dead_code(&self, result: &DeadCodeResult) -> String {
889 let mut output = String::new();
890
891 output.push_str("# Dead Code Analysis\n\n");
892 output.push_str(&format!(
893 "**Found:** {} unused symbols\n\n",
894 result.total_dead
895 ));
896
897 if !result.by_kind.is_empty() {
898 output.push_str("### By Kind\n\n");
899 for (kind, count) in &result.by_kind {
900 output.push_str(&format!("- **{}:** {}\n", kind, count));
901 }
902 output.push('\n');
903 }
904
905 output.push_str("### Unused Symbols\n\n");
906 output.push_str("| Name | Kind | File | Line |\n");
907 output.push_str("|------|------|------|------|\n");
908
909 for sym in &result.symbols {
910 output.push_str(&format!(
911 "| `{}` | {} | `{}` | {} |\n",
912 sym.name, sym.kind, sym.file, sym.line
913 ));
914 }
915
916 output
917 }
918
919 fn format_flow(&self, result: &FlowResult) -> String {
920 let mut output = String::new();
921
922 output.push_str(&format!("# Data Flow: {}\n\n", result.symbol));
923
924 for (i, path) in result.flow_paths.iter().enumerate() {
925 output.push_str(&format!("## Flow Path {}\n\n", i + 1));
926
927 for step in path {
928 output.push_str(&format!(
929 "1. **[{}]** `{}:{}` - `{}`\n",
930 step.action,
931 step.file,
932 step.line,
933 step.expression.trim()
934 ));
935 }
936 output.push('\n');
937 }
938
939 output
940 }
941
942 fn format_impact(&self, result: &ImpactResult) -> String {
943 let mut output = String::new();
944
945 output.push_str(&format!("# Impact Analysis: {}\n\n", result.symbol));
946 output.push_str(&format!("**File:** `{}`\n\n", result.file));
947 output.push_str(&format!("**Risk Level:** {}\n\n", result.risk_level));
948
949 output.push_str(&format!(
950 "## Direct Callers ({})\n\n",
951 result.direct_caller_count
952 ));
953 for caller in &result.direct_callers {
954 output.push_str(&format!("- `{}`\n", caller));
955 }
956 output.push('\n');
957
958 if !result.transitive_callers.is_empty() {
959 output.push_str(&format!(
960 "## Transitive Callers ({})\n\n",
961 result.transitive_caller_count
962 ));
963 for caller in result.transitive_callers.iter().take(20) {
964 output.push_str(&format!("- `{}`\n", caller));
965 }
966 output.push('\n');
967 }
968
969 output.push_str(&format!(
970 "## Affected Entry Points ({})\n\n",
971 result.affected_entry_points.len()
972 ));
973 for ep in &result.affected_entry_points {
974 output.push_str(&format!("- `{}`\n", ep));
975 }
976
977 output
978 }
979
980 fn format_module(&self, result: &ModuleResult) -> String {
981 let mut output = String::new();
982
983 output.push_str(&format!("# Module: {}\n\n", result.module));
984 output.push_str(&format!("**Path:** `{}`\n\n", result.file_path));
985
986 if !result.exports.is_empty() {
987 output.push_str(&format!("## Exports ({})\n\n", result.exports.len()));
988 for export in &result.exports {
989 output.push_str(&format!("- `{}`\n", export));
990 }
991 output.push('\n');
992 }
993
994 if !result.imported_by.is_empty() {
995 output.push_str(&format!(
996 "## Imported By ({})\n\n",
997 result.imported_by.len()
998 ));
999 for importer in &result.imported_by {
1000 output.push_str(&format!("- `{}`\n", importer));
1001 }
1002 output.push('\n');
1003 }
1004
1005 if !result.dependencies.is_empty() {
1006 output.push_str(&format!(
1007 "## Dependencies ({})\n\n",
1008 result.dependencies.len()
1009 ));
1010 for dep in &result.dependencies {
1011 output.push_str(&format!("- `{}`\n", dep));
1012 }
1013 output.push('\n');
1014 }
1015
1016 if !result.circular_deps.is_empty() {
1017 output.push_str(&format!(
1018 "## ⚠️ Circular Dependencies ({})\n\n",
1019 result.circular_deps.len()
1020 ));
1021 for cycle in &result.circular_deps {
1022 output.push_str(&format!("- `{}`\n", cycle));
1023 }
1024 }
1025
1026 output
1027 }
1028
1029 fn format_pattern(&self, result: &PatternResult) -> String {
1030 let mut output = String::new();
1031
1032 output.push_str(&format!("# Pattern: `{}`\n\n", result.pattern));
1033 output.push_str(&format!(
1034 "**Found:** {} matches in {} files\n\n",
1035 result.total_matches,
1036 result.by_file.len()
1037 ));
1038
1039 output.push_str("## Matches\n\n");
1040 output.push_str("| File | Line | Match |\n");
1041 output.push_str("|------|------|-------|\n");
1042
1043 for m in &result.matches {
1044 let match_escaped = m.matched_text.replace('|', "\\|");
1045 output.push_str(&format!(
1046 "| `{}` | {} | `{}` |\n",
1047 m.file, m.line, match_escaped
1048 ));
1049 }
1050
1051 output
1052 }
1053
1054 fn format_scope(&self, result: &ScopeResult) -> String {
1055 let mut output = String::new();
1056
1057 output.push_str(&format!("# Scope at `{}:{}`\n\n", result.file, result.line));
1058
1059 if let Some(ref scope) = result.enclosing_scope {
1060 output.push_str(&format!("**Enclosing:** `{}`\n\n", scope));
1061 }
1062
1063 if !result.local_variables.is_empty() {
1064 output.push_str(&format!(
1065 "## Local Variables ({})\n\n",
1066 result.local_variables.len()
1067 ));
1068 output.push_str("| Name | Type | Defined At |\n");
1069 output.push_str("|------|------|------------|\n");
1070 for var in &result.local_variables {
1071 output.push_str(&format!(
1072 "| `{}` | {} | line {} |\n",
1073 var.name, var.kind, var.defined_at
1074 ));
1075 }
1076 output.push('\n');
1077 }
1078
1079 if !result.imports.is_empty() {
1080 output.push_str(&format!("## Imports ({})\n\n", result.imports.len()));
1081 for import in &result.imports {
1082 output.push_str(&format!("- `{}`\n", import));
1083 }
1084 }
1085
1086 output
1087 }
1088
1089 fn format_stats(&self, result: &StatsResult) -> String {
1090 let mut output = String::new();
1091
1092 output.push_str("# Codebase Statistics\n\n");
1093
1094 output.push_str("## Overview\n\n");
1095 output.push_str("| Metric | Value |\n");
1096 output.push_str("|--------|-------|\n");
1097 output.push_str(&format!("| Files | {} |\n", result.total_files));
1098 output.push_str(&format!("| Symbols | {} |\n", result.total_symbols));
1099 output.push_str(&format!("| Tokens | {} |\n", result.total_tokens));
1100 output.push_str(&format!("| References | {} |\n", result.total_references));
1101 output.push_str(&format!("| Call Edges | {} |\n", result.total_edges));
1102 output.push_str(&format!(
1103 "| Entry Points | {} |\n",
1104 result.total_entry_points
1105 ));
1106 output.push('\n');
1107
1108 output.push_str("## Symbols by Kind\n\n");
1109 output.push_str("| Kind | Count |\n");
1110 output.push_str("|------|-------|\n");
1111 let mut kinds: Vec<_> = result.symbols_by_kind.iter().collect();
1112 kinds.sort_by(|a, b| b.1.cmp(a.1));
1113 for (kind, count) in &kinds {
1114 output.push_str(&format!("| {} | {} |\n", kind, count));
1115 }
1116 output.push('\n');
1117
1118 output.push_str("## Call Graph\n\n");
1119 output.push_str(&format!("- **Max Depth:** {}\n", result.max_call_depth));
1120 output.push_str(&format!("- **Avg Depth:** {:.1}\n", result.avg_call_depth));
1121
1122 output
1123 }
1124}
1125
1126#[cfg(test)]
1127mod tests {
1128 use super::*;
1129 use crate::trace::output::{ChainStep, InvocationPath};
1130
1131 #[test]
1132 fn test_format_trace_plain() {
1133 let formatter = PlainFormatter::new();
1134 let result = TraceResult {
1135 symbol: "validateUser".to_string(),
1136 defined_at: Some("utils/validation.ts:8".to_string()),
1137 kind: "function".to_string(),
1138 invocation_paths: vec![InvocationPath {
1139 entry_point: "POST /api/auth/login".to_string(),
1140 entry_kind: "route".to_string(),
1141 chain: vec![
1142 ChainStep {
1143 symbol: "loginController.handle".to_string(),
1144 file: "auth.controller.ts".to_string(),
1145 line: 8,
1146 column: Some(5),
1147 context: None,
1148 },
1149 ChainStep {
1150 symbol: "validateUser".to_string(),
1151 file: "validation.ts".to_string(),
1152 line: 8,
1153 column: Some(10),
1154 context: None,
1155 },
1156 ],
1157 }],
1158 total_paths: 1,
1159 entry_points: 1,
1160 };
1161
1162 let output = formatter.format_trace(&result);
1163 assert!(output.contains("TRACE: validateUser"));
1164 assert!(output.contains("Defined: utils/validation.ts:8"));
1165 assert!(output.contains("POST /api/auth/login"));
1166 assert!(output.contains("auth.controller.ts:8"));
1167 assert!(!output.contains("\x1b["));
1168 }
1169
1170 #[test]
1171 fn test_format_refs_plain() {
1172 let formatter = PlainFormatter::new();
1173 let result = RefsResult {
1174 symbol: "userId".to_string(),
1175 defined_at: Some("types.ts:5".to_string()),
1176 symbol_kind: None,
1177 references: vec![],
1178 total_refs: 0,
1179 by_kind: std::collections::HashMap::new(),
1180 by_file: std::collections::HashMap::new(),
1181 };
1182
1183 let output = formatter.format_refs(&result);
1184 assert!(output.contains("REFS: userId"));
1185 assert!(!output.contains("\x1b["));
1186 }
1187}