1use super::{
11 DeadCodeResult, FlowResult, ImpactResult, ModuleResult, PatternResult, ReferenceKind,
12 RefsResult, RiskLevel, ScopeResult, StatsResult, TraceFormatter, TraceResult,
13};
14
15#[allow(dead_code)]
21mod colors {
22 pub const RESET: &str = "\x1b[0m";
23 pub const BOLD: &str = "\x1b[1m";
24 pub const DIM: &str = "\x1b[2m";
25
26 pub const GREEN: &str = "\x1b[32m";
27 pub const YELLOW: &str = "\x1b[33m";
28 pub const BLUE: &str = "\x1b[34m";
29 pub const MAGENTA: &str = "\x1b[35m";
30 pub const CYAN: &str = "\x1b[36m";
31 pub const WHITE: &str = "\x1b[37m";
32 pub const RED: &str = "\x1b[31m";
33
34 pub const BG_RED: &str = "\x1b[41m";
35 pub const BG_YELLOW: &str = "\x1b[43m";
36}
37
38mod box_chars {
40 pub const TOP_LEFT: char = '╔';
41 pub const TOP_RIGHT: char = '╗';
42 pub const BOTTOM_LEFT: char = '╚';
43 pub const BOTTOM_RIGHT: char = '╝';
44 pub const HORIZONTAL: char = '═';
45 pub const VERTICAL: char = '║';
46 pub const THIN_HORIZONTAL: char = '━';
47 pub const ARROW_DOWN: &str = "│";
48 pub const ARROW_RIGHT: &str = "→";
49 pub const TARGET: &str = "←";
50}
51
52pub struct AsciiFormatter {
58 width: usize,
59}
60
61impl AsciiFormatter {
62 pub fn new() -> Self {
64 Self {
65 width: Self::detect_terminal_width(),
66 }
67 }
68
69 fn detect_terminal_width() -> usize {
71 if let Ok(cols) = std::env::var("COLUMNS") {
72 if let Ok(width) = cols.parse::<usize>() {
73 return width.min(200).max(60);
74 }
75 }
76
77 if let Ok(cols) = std::env::var("TERM_WIDTH") {
78 if let Ok(width) = cols.parse::<usize>() {
79 return width.min(200).max(60);
80 }
81 }
82
83 80
84 }
85
86 fn draw_header_box<S: AsRef<str>>(&self, lines: &[S]) -> String {
88 let inner_width = self.width - 4;
89 let mut output = String::new();
90
91 output.push(box_chars::TOP_LEFT);
92 for _ in 0..inner_width + 2 {
93 output.push(box_chars::HORIZONTAL);
94 }
95 output.push(box_chars::TOP_RIGHT);
96 output.push('\n');
97
98 for line in lines {
99 output.push(box_chars::VERTICAL);
100 output.push_str(" ");
101 let display_line = self.truncate_or_pad(line.as_ref(), inner_width);
102 output.push_str(&display_line);
103 output.push_str(" ");
104 output.push(box_chars::VERTICAL);
105 output.push('\n');
106 }
107
108 output.push(box_chars::BOTTOM_LEFT);
109 for _ in 0..inner_width + 2 {
110 output.push(box_chars::HORIZONTAL);
111 }
112 output.push(box_chars::BOTTOM_RIGHT);
113 output.push('\n');
114
115 output
116 }
117
118 fn draw_separator(&self, left_text: &str, right_text: &str) -> String {
120 let inner_width = self.width - 2;
121 let left_len = self.visible_len(left_text);
122 let right_len = self.visible_len(right_text);
123 let sep_len = inner_width.saturating_sub(left_len + right_len + 2);
124
125 let mut output = String::new();
126 for _ in 0..inner_width {
127 output.push(box_chars::THIN_HORIZONTAL);
128 }
129 output.push('\n');
130 output.push_str(left_text);
131 for _ in 0..sep_len {
132 output.push(' ');
133 }
134 output.push_str(right_text);
135 output.push('\n');
136 for _ in 0..inner_width {
137 output.push(box_chars::THIN_HORIZONTAL);
138 }
139 output.push('\n');
140
141 output
142 }
143
144 fn truncate_or_pad(&self, s: &str, width: usize) -> String {
146 let visible_len = self.visible_len(s);
147 if visible_len > width {
148 let mut result = String::new();
149 let mut visible_count = 0;
150 let mut chars = s.chars().peekable();
151
152 while let Some(c) = chars.next() {
153 if c == '\x1b' {
154 result.push(c);
155 while let Some(&next) = chars.peek() {
156 result.push(chars.next().unwrap());
157 if next == 'm' {
158 break;
159 }
160 }
161 } else {
162 if visible_count >= width - 3 {
163 result.push_str("...");
164 result.push_str(colors::RESET);
165 break;
166 }
167 result.push(c);
168 visible_count += 1;
169 }
170 }
171 result
172 } else {
173 let padding = width - visible_len;
174 format!("{}{}", s, " ".repeat(padding))
175 }
176 }
177
178 fn visible_len(&self, s: &str) -> usize {
180 let mut len = 0;
181 let mut in_escape = false;
182
183 for c in s.chars() {
184 if c == '\x1b' {
185 in_escape = true;
186 } else if in_escape {
187 if c == 'm' {
188 in_escape = false;
189 }
190 } else {
191 len += 1;
192 }
193 }
194
195 len
196 }
197
198 fn color_ref_kind(&self, kind: ReferenceKind) -> &'static str {
200 match kind {
201 ReferenceKind::Read => colors::CYAN,
202 ReferenceKind::Write => colors::YELLOW,
203 ReferenceKind::Call => colors::GREEN,
204 ReferenceKind::TypeAnnotation => colors::MAGENTA,
205 ReferenceKind::Import => colors::BLUE,
206 ReferenceKind::Export => colors::BLUE,
207 }
208 }
209
210 fn color_risk(&self, risk: RiskLevel) -> &'static str {
212 match risk {
213 RiskLevel::Low => colors::GREEN,
214 RiskLevel::Medium => colors::YELLOW,
215 RiskLevel::High => colors::RED,
216 RiskLevel::Critical => colors::BG_RED,
217 }
218 }
219}
220
221impl Default for AsciiFormatter {
222 fn default() -> Self {
223 Self::new()
224 }
225}
226
227impl TraceFormatter for AsciiFormatter {
228 fn format_trace(&self, result: &TraceResult) -> String {
229 let mut output = String::new();
230
231 let defined_at = result.defined_at.as_deref().unwrap_or("unknown");
232 let header_lines = [
233 &format!(
234 "{}{}TRACE:{} {}",
235 colors::BOLD,
236 colors::CYAN,
237 colors::RESET,
238 result.symbol
239 ),
240 &format!("{}Defined:{} {}", colors::DIM, colors::RESET, defined_at),
241 &format!(
242 "{}Found:{} {} invocation paths from {} entry points",
243 colors::DIM,
244 colors::RESET,
245 result.total_paths,
246 result.entry_points
247 ),
248 ];
249 output.push_str(&self.draw_header_box(&header_lines));
250 output.push('\n');
251
252 for (i, path) in result.invocation_paths.iter().enumerate() {
253 let path_header = format!(
254 "{}{}Path {}/{}{}",
255 colors::BOLD,
256 colors::WHITE,
257 i + 1,
258 result.total_paths,
259 colors::RESET
260 );
261 let entry_info = format!("{}{}{}", colors::GREEN, path.entry_point, colors::RESET);
262 output.push_str(&self.draw_separator(&path_header, &entry_info));
263 output.push('\n');
264
265 let max_file_width = path
266 .chain
267 .iter()
268 .map(|s| s.file.len() + format!(":{}", s.line).len())
269 .max()
270 .unwrap_or(20);
271
272 for (j, step) in path.chain.iter().enumerate() {
273 let is_target = j == path.chain.len() - 1;
274 let location = format!("{}:{}", step.file, step.line);
275 let padding = max_file_width.saturating_sub(location.len()) + 2;
276
277 if is_target {
278 output.push_str(&format!(
279 " {}{:<width$}{} {} {}{}{} {}{} TARGET{}",
280 colors::DIM,
281 location,
282 colors::RESET,
283 box_chars::ARROW_RIGHT,
284 colors::BOLD,
285 colors::GREEN,
286 step.symbol,
287 colors::YELLOW,
288 box_chars::TARGET,
289 colors::RESET,
290 width = max_file_width + padding
291 ));
292 } else {
293 output.push_str(&format!(
294 " {}{:<width$}{} {} {}{}{}",
295 colors::DIM,
296 location,
297 colors::RESET,
298 box_chars::ARROW_RIGHT,
299 colors::CYAN,
300 step.symbol,
301 colors::RESET,
302 width = max_file_width + padding
303 ));
304 }
305 output.push('\n');
306
307 if let Some(ref ctx) = step.context {
309 for line in ctx.lines() {
310 output.push_str(&format!(
311 " {}{}{}\n",
312 colors::DIM,
313 line,
314 colors::RESET
315 ));
316 }
317 }
318
319 if !is_target {
320 output.push_str(&format!(
321 " {}{:<width$}{} {}",
322 colors::DIM,
323 "",
324 colors::RESET,
325 box_chars::ARROW_DOWN,
326 width = max_file_width + padding
327 ));
328 output.push('\n');
329 }
330 }
331 output.push('\n');
332 }
333
334 output
335 }
336
337 fn format_refs(&self, result: &RefsResult) -> String {
338 let mut output = String::new();
339
340 let defined_at = result.defined_at.as_deref().unwrap_or("unknown");
341 let header_lines = [
342 &format!(
343 "{}{}REFS:{} {}",
344 colors::BOLD,
345 colors::CYAN,
346 colors::RESET,
347 result.symbol
348 ),
349 &format!("{}Defined:{} {}", colors::DIM, colors::RESET, defined_at),
350 &format!(
351 "{}Found:{} {} references",
352 colors::DIM,
353 colors::RESET,
354 result.total_refs
355 ),
356 ];
357 output.push_str(&self.draw_header_box(&header_lines));
358 output.push('\n');
359
360 if !result.by_kind.is_empty() {
361 output.push_str(&format!("{}By kind:{} ", colors::DIM, colors::RESET));
362 let kinds: Vec<_> = result
363 .by_kind
364 .iter()
365 .map(|(k, v)| format!("{}={}", k, v))
366 .collect();
367 output.push_str(&kinds.join(", "));
368 output.push_str("\n\n");
369 }
370
371 let mut by_file: std::collections::HashMap<&str, Vec<_>> = std::collections::HashMap::new();
372 for r in &result.references {
373 by_file.entry(&r.file).or_default().push(r);
374 }
375
376 for (file, refs) in by_file {
377 output.push_str(&format!(
378 "{}{}{}:{}\n",
379 colors::BOLD,
380 colors::WHITE,
381 file,
382 colors::RESET
383 ));
384
385 for r in refs {
386 let kind_color = self.color_ref_kind(r.kind);
387 output.push_str(&format!(
388 " {}:{:<4} {}{:<6}{} ",
389 r.line,
390 r.column,
391 kind_color,
392 r.kind,
393 colors::RESET,
394 ));
395
396 let context_lines: Vec<&str> = r.context.lines().collect();
398 if context_lines.len() > 1 {
399 output.push('\n');
400 for line in &context_lines {
401 output.push_str(&format!(" {}\n", line));
402 }
403 } else {
404 output.push_str(r.context.trim());
405 output.push('\n');
406 }
407
408 if let Some(ref enclosing) = r.enclosing_symbol {
409 output.push_str(&format!(
410 " {}(in {}){}",
411 colors::DIM,
412 enclosing,
413 colors::RESET
414 ));
415 output.push('\n');
416 }
417 }
418 output.push('\n');
419 }
420
421 output
422 }
423
424 fn format_dead_code(&self, result: &DeadCodeResult) -> String {
425 let mut output = String::new();
426
427 let header_lines = [
428 &format!(
429 "{}{}DEAD CODE ANALYSIS{}",
430 colors::BOLD,
431 colors::YELLOW,
432 colors::RESET
433 ),
434 &format!(
435 "{}Found:{} {} unused symbols",
436 colors::DIM,
437 colors::RESET,
438 result.total_dead
439 ),
440 ];
441 output.push_str(&self.draw_header_box(&header_lines));
442 output.push('\n');
443
444 if !result.by_kind.is_empty() {
445 output.push_str(&format!("{}By kind:{} ", colors::DIM, colors::RESET));
446 let kinds: Vec<_> = result
447 .by_kind
448 .iter()
449 .map(|(k, v)| format!("{}={}", k, v))
450 .collect();
451 output.push_str(&kinds.join(", "));
452 output.push_str("\n\n");
453 }
454
455 for sym in &result.symbols {
456 output.push_str(&format!(
457 " {}{}{} {}{}:{}{} {}\n",
458 colors::YELLOW,
459 sym.name,
460 colors::RESET,
461 colors::DIM,
462 sym.file,
463 sym.line,
464 colors::RESET,
465 sym.reason
466 ));
467
468 if !sym.potential_callers.is_empty() {
470 output.push_str(&format!(
471 " {}Potential callers:{}\n",
472 colors::DIM,
473 colors::RESET
474 ));
475 for caller in &sym.potential_callers {
476 output.push_str(&format!(
477 " {}→{} {} {}{}:{}{} {}{}{}\n",
478 colors::GREEN,
479 colors::RESET,
480 caller.name,
481 colors::DIM,
482 caller.file,
483 caller.line,
484 colors::RESET,
485 colors::DIM,
486 caller.reason,
487 colors::RESET
488 ));
489 }
490 }
491 }
492
493 output
494 }
495
496 fn format_flow(&self, result: &FlowResult) -> String {
497 let mut output = String::new();
498
499 let header_lines = [
500 &format!(
501 "{}{}DATA FLOW:{} {}",
502 colors::BOLD,
503 colors::MAGENTA,
504 colors::RESET,
505 result.symbol
506 ),
507 &format!(
508 "{}Paths:{} {}",
509 colors::DIM,
510 colors::RESET,
511 result.flow_paths.len()
512 ),
513 ];
514 output.push_str(&self.draw_header_box(&header_lines));
515 output.push('\n');
516
517 for (i, path) in result.flow_paths.iter().enumerate() {
518 output.push_str(&format!(
519 "{}{}Flow Path {}{}:\n",
520 colors::BOLD,
521 colors::WHITE,
522 i + 1,
523 colors::RESET
524 ));
525
526 for step in path {
527 let action_color = match step.action {
528 super::FlowAction::Define | super::FlowAction::Assign => colors::GREEN,
529 super::FlowAction::Read => colors::CYAN,
530 super::FlowAction::PassToFunction => colors::YELLOW,
531 super::FlowAction::ReturnFrom => colors::MAGENTA,
532 super::FlowAction::Mutate => colors::RED,
533 };
534
535 output.push_str(&format!(
536 " {}:{:<4} {}{:<8}{} {}\n",
537 step.file,
538 step.line,
539 action_color,
540 step.action,
541 colors::RESET,
542 step.expression.trim()
543 ));
544 }
545 output.push('\n');
546 }
547
548 output
549 }
550
551 fn format_impact(&self, result: &ImpactResult) -> String {
552 let mut output = String::new();
553
554 let risk_color = self.color_risk(result.risk_level);
555 let header_lines = [
556 &format!(
557 "{}{}IMPACT ANALYSIS:{} {}",
558 colors::BOLD,
559 colors::RED,
560 colors::RESET,
561 result.symbol
562 ),
563 &format!("{}File:{} {}", colors::DIM, colors::RESET, result.file),
564 &format!(
565 "{}Risk Level:{} {}{}{}{}",
566 colors::DIM,
567 colors::RESET,
568 colors::BOLD,
569 risk_color,
570 result.risk_level,
571 colors::RESET
572 ),
573 ];
574 output.push_str(&self.draw_header_box(&header_lines));
575 output.push('\n');
576
577 output.push_str(&format!(
578 "{}Direct callers ({}):{}\n",
579 colors::BOLD,
580 result.direct_caller_count,
581 colors::RESET
582 ));
583 for caller in &result.direct_callers {
584 output.push_str(&format!(" {} {}\n", box_chars::ARROW_RIGHT, caller));
585 }
586 output.push('\n');
587
588 if !result.transitive_callers.is_empty() {
589 output.push_str(&format!(
590 "{}Transitive callers ({}):{}\n",
591 colors::BOLD,
592 result.transitive_caller_count,
593 colors::RESET
594 ));
595 for caller in result.transitive_callers.iter().take(10) {
596 output.push_str(&format!(
597 " {} {}{}{}\n",
598 box_chars::ARROW_RIGHT,
599 colors::DIM,
600 caller,
601 colors::RESET
602 ));
603 }
604 if result.transitive_callers.len() > 10 {
605 output.push_str(&format!(
606 " {}... and {} more{}\n",
607 colors::DIM,
608 result.transitive_callers.len() - 10,
609 colors::RESET
610 ));
611 }
612 output.push('\n');
613 }
614
615 output.push_str(&format!(
616 "{}Affected entry points ({}):{}\n",
617 colors::BOLD,
618 result.affected_entry_points.len(),
619 colors::RESET
620 ));
621 for ep in &result.affected_entry_points {
622 output.push_str(&format!(
623 " {} {}{}{}\n",
624 box_chars::ARROW_RIGHT,
625 colors::GREEN,
626 ep,
627 colors::RESET
628 ));
629 }
630
631 output.push_str(&format!(
632 "\n{}Files affected:{} {}\n",
633 colors::DIM,
634 colors::RESET,
635 result.files_affected.len()
636 ));
637
638 output
639 }
640
641 fn format_module(&self, result: &ModuleResult) -> String {
642 let mut output = String::new();
643
644 let header_lines = [
645 &format!(
646 "{}{}MODULE:{} {}",
647 colors::BOLD,
648 colors::BLUE,
649 colors::RESET,
650 result.module
651 ),
652 &format!("{}Path:{} {}", colors::DIM, colors::RESET, result.file_path),
653 ];
654 output.push_str(&self.draw_header_box(&header_lines));
655 output.push('\n');
656
657 if !result.exports.is_empty() {
658 output.push_str(&format!(
659 "{}Exports ({}):{}\n",
660 colors::BOLD,
661 result.exports.len(),
662 colors::RESET
663 ));
664 for export in &result.exports {
665 output.push_str(&format!(
666 " {} {}{}{}\n",
667 box_chars::ARROW_RIGHT,
668 colors::GREEN,
669 export,
670 colors::RESET
671 ));
672 }
673 output.push('\n');
674 }
675
676 if !result.imported_by.is_empty() {
677 output.push_str(&format!(
678 "{}Imported by ({}):{}\n",
679 colors::BOLD,
680 result.imported_by.len(),
681 colors::RESET
682 ));
683 for importer in &result.imported_by {
684 output.push_str(&format!(" {} {}\n", box_chars::ARROW_RIGHT, importer));
685 }
686 output.push('\n');
687 }
688
689 if !result.dependencies.is_empty() {
690 output.push_str(&format!(
691 "{}Dependencies ({}):{}\n",
692 colors::BOLD,
693 result.dependencies.len(),
694 colors::RESET
695 ));
696 for dep in &result.dependencies {
697 output.push_str(&format!(
698 " {} {}{}{}\n",
699 box_chars::ARROW_RIGHT,
700 colors::CYAN,
701 dep,
702 colors::RESET
703 ));
704 }
705 output.push('\n');
706 }
707
708 if !result.circular_deps.is_empty() {
709 output.push_str(&format!(
710 "{}{}CIRCULAR DEPENDENCIES ({}):{}\n",
711 colors::BOLD,
712 colors::RED,
713 result.circular_deps.len(),
714 colors::RESET
715 ));
716 for cycle in &result.circular_deps {
717 output.push_str(&format!(
718 " {}⚠ {}{}\n",
719 colors::YELLOW,
720 cycle,
721 colors::RESET
722 ));
723 }
724 }
725
726 output
727 }
728
729 fn format_pattern(&self, result: &PatternResult) -> String {
730 let mut output = String::new();
731
732 let header_lines = [
733 &format!(
734 "{}{}PATTERN:{} {}",
735 colors::BOLD,
736 colors::MAGENTA,
737 colors::RESET,
738 result.pattern
739 ),
740 &format!(
741 "{}Found:{} {} matches in {} files",
742 colors::DIM,
743 colors::RESET,
744 result.total_matches,
745 result.by_file.len()
746 ),
747 ];
748 output.push_str(&self.draw_header_box(&header_lines));
749 output.push('\n');
750
751 let mut by_file: std::collections::HashMap<&str, Vec<_>> = std::collections::HashMap::new();
752 for m in &result.matches {
753 by_file.entry(&m.file).or_default().push(m);
754 }
755
756 for (file, matches) in by_file {
757 output.push_str(&format!(
758 "{}{}{}:{}\n",
759 colors::BOLD,
760 colors::WHITE,
761 file,
762 colors::RESET
763 ));
764
765 for m in matches {
766 let context_lines: Vec<&str> = m.context.lines().collect();
768 if context_lines.len() > 1 {
769 for line in &context_lines {
770 output.push_str(&format!(" {}\n", line));
771 }
772 } else {
773 output.push_str(&format!(
774 " {}:{:<4} {}\n",
775 m.line,
776 m.column,
777 m.context.trim()
778 ));
779 }
780
781 if let Some(ref enclosing) = m.enclosing_symbol {
782 output.push_str(&format!(
783 " {}(in {}){}",
784 colors::DIM,
785 enclosing,
786 colors::RESET
787 ));
788 output.push('\n');
789 }
790 }
791 output.push('\n');
792 }
793
794 output
795 }
796
797 fn format_scope(&self, result: &ScopeResult) -> String {
798 let mut output = String::new();
799
800 let scope_name = result.enclosing_scope.as_deref().unwrap_or("<global>");
801 let header_lines = [
802 &format!(
803 "{}{}SCOPE AT:{} {}:{}",
804 colors::BOLD,
805 colors::CYAN,
806 colors::RESET,
807 result.file,
808 result.line
809 ),
810 &format!("{}Enclosing:{} {}", colors::DIM, colors::RESET, scope_name),
811 ];
812 output.push_str(&self.draw_header_box(&header_lines));
813 output.push('\n');
814
815 if !result.local_variables.is_empty() {
816 output.push_str(&format!(
817 "{}Local Variables ({}):{}\n",
818 colors::BOLD,
819 result.local_variables.len(),
820 colors::RESET
821 ));
822 for var in &result.local_variables {
823 output.push_str(&format!(
824 " {}{}{}: {} {}(line {}){}",
825 colors::CYAN,
826 var.name,
827 colors::RESET,
828 var.kind,
829 colors::DIM,
830 var.defined_at,
831 colors::RESET
832 ));
833 output.push('\n');
834 }
835 output.push('\n');
836 }
837
838 if !result.parameters.is_empty() {
839 output.push_str(&format!(
840 "{}Parameters ({}):{}\n",
841 colors::BOLD,
842 result.parameters.len(),
843 colors::RESET
844 ));
845 for param in &result.parameters {
846 output.push_str(&format!(
847 " {}{}{}: {}\n",
848 colors::YELLOW,
849 param.name,
850 colors::RESET,
851 param.kind
852 ));
853 }
854 output.push('\n');
855 }
856
857 if !result.imports.is_empty() {
858 output.push_str(&format!(
859 "{}Imports ({}):{}\n",
860 colors::BOLD,
861 result.imports.len(),
862 colors::RESET
863 ));
864 for import in &result.imports {
865 output.push_str(&format!(" {}{}{}\n", colors::BLUE, import, colors::RESET));
866 }
867 }
868
869 output
870 }
871
872 fn format_stats(&self, result: &StatsResult) -> String {
873 let mut output = String::new();
874
875 let header_lines = [&format!(
876 "{}{}CODEBASE STATISTICS{}",
877 colors::BOLD,
878 colors::GREEN,
879 colors::RESET
880 )];
881 output.push_str(&self.draw_header_box(&header_lines));
882 output.push('\n');
883
884 output.push_str(&format!("{}Overview:{}\n", colors::BOLD, colors::RESET));
886 output.push_str(&format!(" Files: {}\n", result.total_files));
887 output.push_str(&format!(" Symbols: {}\n", result.total_symbols));
888 output.push_str(&format!(" Tokens: {}\n", result.total_tokens));
889 output.push_str(&format!(" References: {}\n", result.total_references));
890 output.push_str(&format!(" Call Edges: {}\n", result.total_edges));
891 output.push_str(&format!(" Entry Points: {}\n", result.total_entry_points));
892 output.push('\n');
893
894 output.push_str(&format!(
896 "{}Files by extension:{}\n",
897 colors::BOLD,
898 colors::RESET
899 ));
900 let mut exts: Vec<_> = result.files_by_extension.iter().collect();
901 exts.sort_by(|a, b| b.1.cmp(a.1));
902 for (ext, count) in exts.iter().take(10) {
903 output.push_str(&format!(" .{}: {}\n", ext, count));
904 }
905 output.push('\n');
906
907 output.push_str(&format!(
909 "{}Symbols by kind:{}\n",
910 colors::BOLD,
911 colors::RESET
912 ));
913 let mut kinds: Vec<_> = result.symbols_by_kind.iter().collect();
914 kinds.sort_by(|a, b| b.1.cmp(a.1));
915 for (kind, count) in &kinds {
916 output.push_str(&format!(" {}: {}\n", kind, count));
917 }
918 output.push('\n');
919
920 output.push_str(&format!("{}Call Graph:{}\n", colors::BOLD, colors::RESET));
922 output.push_str(&format!(" Max Call Depth: {}\n", result.max_call_depth));
923 output.push_str(&format!(" Avg Call Depth: {:.1}\n", result.avg_call_depth));
924 output.push('\n');
925
926 if !result.most_referenced.is_empty() {
928 output.push_str(&format!(
929 "{}Most Referenced Symbols:{}\n",
930 colors::BOLD,
931 colors::RESET
932 ));
933 for (name, count) in result.most_referenced.iter().take(10) {
934 output.push_str(&format!(
935 " {}{}{}: {} refs\n",
936 colors::CYAN,
937 name,
938 colors::RESET,
939 count
940 ));
941 }
942 output.push('\n');
943 }
944
945 if !result.largest_files.is_empty() {
947 output.push_str(&format!(
948 "{}Largest Files (by symbols):{}\n",
949 colors::BOLD,
950 colors::RESET
951 ));
952 for (file, count) in result.largest_files.iter().take(10) {
953 output.push_str(&format!(" {}: {} symbols\n", file, count));
954 }
955 }
956
957 output
958 }
959}
960
961#[cfg(test)]
962mod tests {
963 use super::*;
964
965 #[test]
966 fn test_visible_len() {
967 let formatter = AsciiFormatter::new();
968 assert_eq!(formatter.visible_len("hello"), 5);
969 assert_eq!(formatter.visible_len("\x1b[32mhello\x1b[0m"), 5);
970 assert_eq!(formatter.visible_len("\x1b[1m\x1b[32mtest\x1b[0m"), 4);
971 }
972
973 #[test]
974 fn test_format_trace_basic() {
975 let formatter = AsciiFormatter::new();
976 let result = TraceResult {
977 symbol: "validateUser".to_string(),
978 defined_at: Some("utils/validation.ts:8".to_string()),
979 kind: "function".to_string(),
980 invocation_paths: vec![],
981 total_paths: 0,
982 entry_points: 0,
983 };
984 let output = formatter.format_trace(&result);
985 assert!(output.contains("validateUser"));
986 assert!(output.contains("TRACE"));
987 }
988}