1use crate::ai::trace_prompts::is_natural_language_query;
9use crate::ai::{claude::ClaudeClient, gemini::GeminiClient};
10use crate::auth::{self, Provider};
11use crate::core::error::{Error, Result};
12use crate::core::project::Project;
13use crate::trace::context::FileCache;
14use crate::trace::output::{
15 create_formatter, ChainStep, DeadCodeResult, DeadSymbol, FlowAction, FlowResult, FlowStep,
16 ImpactResult, InvocationPath, ModuleResult, OutputFormat, PatternMatch, PatternResult,
17 PotentialCaller, ReferenceInfo, ReferenceKind, RefsResult, RiskLevel, ScopeResult,
18 ScopeVariable, StatsResult, TraceResult,
19};
20use crate::trace::{
21 find_dead_symbols, find_refs, load_index, trace_index_exists, trace_index_path,
22 trace_symbol_by_name, RefKind, SemanticIndex, SymbolKind,
23};
24use clap::Args;
25use regex::Regex;
26use serde::Serialize;
27use std::collections::{HashMap, HashSet};
28use std::env;
29use std::path::{Path, PathBuf};
30use tracing::{debug, info, warn};
31
32#[derive(Debug, Default, Serialize)]
34pub struct CombinedResults {
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub trace: Option<TraceResult>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub refs: Option<RefsResult>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub callers: Option<TraceResult>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub callees: Option<TraceResult>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub type_usage: Option<RefsResult>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub module: Option<ModuleResult>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub pattern: Option<PatternResult>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub flow: Option<FlowResult>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub impact: Option<ImpactResult>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub scope: Option<ScopeResult>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub dead_code: Option<DeadCodeResult>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub stats: Option<StatsResult>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub cycles: Option<ModuleResult>,
61}
62
63fn symbol_kind_str(kind: SymbolKind) -> &'static str {
69 match kind {
70 SymbolKind::Function => "function",
71 SymbolKind::Method => "method",
72 SymbolKind::Class => "class",
73 SymbolKind::Struct => "struct",
74 SymbolKind::Enum => "enum",
75 SymbolKind::Interface => "interface",
76 SymbolKind::TypeAlias => "type_alias",
77 SymbolKind::Constant => "constant",
78 SymbolKind::Variable => "variable",
79 SymbolKind::Module => "module",
80 SymbolKind::Unknown => "unknown",
81 }
82}
83
84fn reference_kind_str(kind: ReferenceKind) -> &'static str {
86 match kind {
87 ReferenceKind::Read => "read",
88 ReferenceKind::Write => "write",
89 ReferenceKind::Call => "call",
90 ReferenceKind::TypeAnnotation => "type",
91 ReferenceKind::Import => "import",
92 ReferenceKind::Export => "export",
93 }
94}
95
96#[derive(Args, Debug)]
102#[command(after_help = "EXAMPLES:
103 greppy trace validateUser Trace invocation paths
104 greppy trace -d validateUser Direct mode (no AI reranking)
105 greppy trace --refs userId Find all references
106 greppy trace --refs userId -c 2 Find refs with 2 lines context
107 greppy trace --refs userId --in src/ Limit to src/ directory
108 greppy trace --reads userId Find reads only
109 greppy trace --writes userId Find writes only
110 greppy trace --callers fetchData Show what calls this
111 greppy trace --callees fetchData Show what this calls
112 greppy trace --type UserProfile Trace type usage
113 greppy trace --module @/lib/auth Trace module imports/exports
114 greppy trace --pattern \"TODO:.*\" Find pattern occurrences
115 greppy trace --flow userInput Trace data flow
116 greppy trace --impact login Analyze change impact
117 greppy trace --scope src/api.ts:42 Show scope at location
118 greppy trace --dead Find unused code
119 greppy trace --dead --xref Dead code with potential callers
120 greppy trace --stats Show codebase statistics
121 greppy trace --cycles Find circular dependencies
122
123COMPOSABLE FLAGS (run multiple operations at once):
124 greppy trace --dead --stats Dead code + statistics
125 greppy trace --dead --stats --in src/ Filtered to src/ directory
126 greppy trace --dead --stats --summary Condensed one-line summaries
127 greppy trace --refs foo --impact foo References + impact analysis
128 greppy trace --dead --cycles Dead code + circular deps
129
130FILTERING:
131 greppy trace --dead --in src/auth Filter to path
132 greppy trace --dead --symbol-type fn Filter by type (fn, struct, etc)
133 greppy trace --dead --name \"test.*\" Filter by name pattern
134
135OUTPUT FORMATS:
136 greppy trace --refs userId --json JSON output for tooling
137 greppy trace --dead --stats --json Combined JSON for multi-op
138 greppy trace --refs userId --plain Plain text (no colors)
139 greppy trace --refs userId --csv CSV output
140 greppy trace --refs userId --dot DOT graph format
141 greppy trace --refs userId --markdown Markdown output")]
142pub struct TraceArgs {
143 pub symbol: Option<String>,
145
146 #[arg(short = 'd', long)]
148 pub direct: bool,
149
150 #[arg(long, value_name = "SYMBOL")]
152 pub refs: Option<String>,
153
154 #[arg(long, value_name = "SYMBOL")]
156 pub reads: Option<String>,
157
158 #[arg(long, value_name = "SYMBOL")]
160 pub writes: Option<String>,
161
162 #[arg(long, value_name = "SYMBOL")]
164 pub callers: Option<String>,
165
166 #[arg(long, value_name = "SYMBOL")]
168 pub callees: Option<String>,
169
170 #[arg(long = "type", value_name = "TYPE")]
172 pub type_name: Option<String>,
173
174 #[arg(long, value_name = "MODULE")]
176 pub module: Option<String>,
177
178 #[arg(long, value_name = "REGEX")]
180 pub pattern: Option<String>,
181
182 #[arg(long, value_name = "SYMBOL")]
184 pub flow: Option<String>,
185
186 #[arg(long, value_name = "SYMBOL")]
188 pub impact: Option<String>,
189
190 #[arg(long, value_name = "LOCATION")]
192 pub scope: Option<String>,
193
194 #[arg(long)]
196 pub dead: bool,
197
198 #[arg(long)]
200 pub xref: bool,
201
202 #[arg(long)]
204 pub stats: bool,
205
206 #[arg(long)]
208 pub cycles: bool,
209
210 #[arg(long, value_name = "KIND")]
212 pub kind: Option<String>,
213
214 #[arg(long, value_name = "PATH")]
216 pub r#in: Option<PathBuf>,
217
218 #[arg(long, value_name = "TYPE")]
220 pub symbol_type: Option<String>,
221
222 #[arg(long, value_name = "PATTERN")]
224 pub name: Option<String>,
225
226 #[arg(long, value_name = "GROUP")]
228 pub group_by: Option<String>,
229
230 #[arg(long)]
232 pub json: bool,
233
234 #[arg(long)]
236 pub plain: bool,
237
238 #[arg(long)]
240 pub csv: bool,
241
242 #[arg(long)]
244 pub dot: bool,
245
246 #[arg(long)]
248 pub markdown: bool,
249
250 #[arg(long)]
252 pub tui: bool,
253
254 #[arg(long, default_value = "10")]
256 pub max_depth: usize,
257
258 #[arg(long, short = 'c', default_value = "0")]
260 pub context: u32,
261
262 #[arg(long, short = 'n')]
264 pub limit: Option<usize>,
265
266 #[arg(long)]
268 pub count: bool,
269
270 #[arg(long)]
272 pub summary: bool,
273
274 #[arg(short, long)]
276 pub project: Option<PathBuf>,
277}
278
279impl TraceArgs {
280 fn output_format(&self) -> OutputFormat {
282 if self.json {
283 OutputFormat::Json
284 } else if self.csv {
285 OutputFormat::Csv
286 } else if self.dot {
287 OutputFormat::Dot
288 } else if self.markdown {
289 OutputFormat::Markdown
290 } else if self.plain {
291 OutputFormat::Plain
292 } else {
293 OutputFormat::Ascii
294 }
295 }
296
297 fn operations(&self) -> Vec<TraceOperation> {
299 let mut ops = Vec::new();
300
301 if self.dead {
303 ops.push(TraceOperation::DeadCode);
304 }
305 if self.stats {
306 ops.push(TraceOperation::Stats);
307 }
308 if self.cycles {
309 ops.push(TraceOperation::Cycles);
310 }
311
312 if let Some(ref loc) = self.scope {
314 ops.push(TraceOperation::Scope(loc.clone()));
315 }
316 if let Some(ref sym) = self.impact {
317 ops.push(TraceOperation::Impact(sym.clone()));
318 }
319 if let Some(ref sym) = self.flow {
320 ops.push(TraceOperation::Flow(sym.clone()));
321 }
322 if let Some(ref pattern) = self.pattern {
323 ops.push(TraceOperation::Pattern(pattern.clone()));
324 }
325 if let Some(ref module) = self.module {
326 ops.push(TraceOperation::Module(module.clone()));
327 }
328 if let Some(ref type_name) = self.type_name {
329 ops.push(TraceOperation::Type(type_name.clone()));
330 }
331 if let Some(ref sym) = self.callers {
332 ops.push(TraceOperation::Callers(sym.clone()));
333 }
334 if let Some(ref sym) = self.callees {
335 ops.push(TraceOperation::Callees(sym.clone()));
336 }
337 if let Some(ref sym) = self.reads {
338 ops.push(TraceOperation::Refs {
339 symbol: sym.clone(),
340 kind: Some(ReferenceKind::Read),
341 });
342 }
343 if let Some(ref sym) = self.writes {
344 ops.push(TraceOperation::Refs {
345 symbol: sym.clone(),
346 kind: Some(ReferenceKind::Write),
347 });
348 }
349 if let Some(ref sym) = self.refs {
350 ops.push(TraceOperation::Refs {
351 symbol: sym.clone(),
352 kind: self.parse_kind_filter(),
353 });
354 }
355 if let Some(ref sym) = self.symbol {
356 ops.push(TraceOperation::Trace(sym.clone()));
357 }
358
359 ops
360 }
361
362 fn parse_kind_filter(&self) -> Option<ReferenceKind> {
364 self.kind
365 .as_ref()
366 .and_then(|k| match k.to_lowercase().as_str() {
367 "read" => Some(ReferenceKind::Read),
368 "write" => Some(ReferenceKind::Write),
369 "call" => Some(ReferenceKind::Call),
370 "type" => Some(ReferenceKind::TypeAnnotation),
371 "import" => Some(ReferenceKind::Import),
372 "export" => Some(ReferenceKind::Export),
373 _ => None,
374 })
375 }
376}
377
378#[derive(Debug)]
380enum TraceOperation {
381 Trace(String),
382 Refs {
383 symbol: String,
384 kind: Option<ReferenceKind>,
385 },
386 Callers(String),
387 Callees(String),
388 Type(String),
389 Module(String),
390 Pattern(String),
391 Flow(String),
392 Impact(String),
393 Scope(String),
394 DeadCode,
395 Stats,
396 Cycles,
397}
398
399#[derive(Debug, Clone, Default)]
401pub struct TraceFilter {
402 pub path: Option<String>,
404 pub symbol_type: Option<String>,
406 pub name_pattern: Option<regex::Regex>,
408}
409
410impl TraceFilter {
411 pub fn matches_symbol(&self, name: &str, kind: &str, file_path: &str) -> bool {
413 if let Some(ref path) = self.path {
415 if !file_path.contains(path) {
416 return false;
417 }
418 }
419 if let Some(ref stype) = self.symbol_type {
421 if !kind.to_lowercase().contains(&stype.to_lowercase()) {
422 return false;
423 }
424 }
425 if let Some(ref pattern) = self.name_pattern {
427 if !pattern.is_match(name) {
428 return false;
429 }
430 }
431 true
432 }
433
434 pub fn matches_path(&self, file_path: &str) -> bool {
436 if let Some(ref path) = self.path {
437 return file_path.contains(path);
438 }
439 true
440 }
441}
442
443impl TraceArgs {
444 pub fn build_filter(&self) -> TraceFilter {
446 TraceFilter {
447 path: self.r#in.as_ref().map(|p| p.to_string_lossy().to_string()),
448 symbol_type: self.symbol_type.clone(),
449 name_pattern: self.name.as_ref().and_then(|p| regex::Regex::new(p).ok()),
450 }
451 }
452}
453
454pub async fn run(args: TraceArgs) -> Result<()> {
460 let project_path = args
461 .project
462 .clone()
463 .unwrap_or_else(|| env::current_dir().expect("Failed to get current directory"));
464
465 let project = Project::detect(&project_path)?;
466 let format = args.output_format();
467 let formatter = create_formatter(format);
468
469 if args.tui {
471 return run_tui(&args, &project).await;
472 }
473
474 let operations = args.operations();
476 debug!(?operations, "Trace operations");
477
478 let filter = args.build_filter();
480
481 if operations.is_empty() {
483 eprintln!("Usage: greppy trace <symbol>");
484 eprintln!(" greppy trace --refs <symbol>");
485 eprintln!(" greppy trace --dead");
486 eprintln!(" greppy trace --stats");
487 eprintln!(" greppy trace --dead --stats (composable!)");
488 eprintln!("Run 'greppy trace --help' for more options.");
489 return Err(Error::SearchError {
490 message: "No symbol or operation specified".to_string(),
491 });
492 }
493
494 let multi_op = operations.len() > 1;
495 let summary_mode = args.summary;
496 let json_multi_op = args.json && multi_op;
497
498 let mut combined = CombinedResults::default();
500
501 for (i, operation) in operations.iter().enumerate() {
503 if (multi_op || summary_mode) && !json_multi_op {
505 if i > 0 {
506 println!();
507 }
508 let header = operation_header(operation);
509 println!("{}", "═".repeat(79));
510 println!("{}", header);
511 println!("{}", "═".repeat(79));
512 }
513
514 match operation {
515 TraceOperation::Trace(symbol) => {
516 info!(symbol = %symbol, "Tracing symbol invocations");
517 let result =
518 trace_symbol_cmd(&project, symbol, args.max_depth, args.direct, &filter)
519 .await?;
520 if json_multi_op {
521 combined.trace = Some(result);
522 } else if summary_mode {
523 println!(
524 " Paths: {} Entry points: {}",
525 result.invocation_paths.len(),
526 result.entry_points
527 );
528 } else {
529 println!("{}", formatter.format_trace(&result));
530 }
531 }
532 TraceOperation::Refs { symbol, kind } => {
533 info!(symbol = %symbol, ?kind, "Finding references");
534 let result = find_refs_cmd(&project, symbol, *kind, &args, &filter).await?;
535 if json_multi_op {
536 combined.refs = Some(result);
537 } else if args.count || summary_mode {
538 println!(
539 " References: {} Files: {}",
540 result.total_refs,
541 result.by_file.len()
542 );
543 } else {
544 println!("{}", formatter.format_refs(&result));
545 }
546 }
547 TraceOperation::Callers(symbol) => {
548 info!(symbol = %symbol, "Finding callers");
549 let result = find_callers_cmd(&project, symbol, args.max_depth, &filter).await?;
550 if json_multi_op {
551 combined.callers = Some(result);
552 } else if summary_mode {
553 println!(
554 " Callers: {} Paths: {}",
555 result.entry_points,
556 result.invocation_paths.len()
557 );
558 } else {
559 println!("{}", formatter.format_trace(&result));
560 }
561 }
562 TraceOperation::Callees(symbol) => {
563 info!(symbol = %symbol, "Finding callees");
564 let result = find_callees_cmd(&project, symbol, args.max_depth, &filter).await?;
565 if json_multi_op {
566 combined.callees = Some(result);
567 } else if summary_mode {
568 println!(" Callees: {}", result.invocation_paths.len());
569 } else {
570 println!("{}", formatter.format_trace(&result));
571 }
572 }
573 TraceOperation::Type(type_name) => {
574 info!(type_name = %type_name, "Tracing type usage");
575 let result = find_refs_cmd(
576 &project,
577 type_name,
578 Some(ReferenceKind::TypeAnnotation),
579 &args,
580 &filter,
581 )
582 .await?;
583 if json_multi_op {
584 combined.type_usage = Some(result);
585 } else if summary_mode {
586 println!(" Type usages: {}", result.total_refs);
587 } else {
588 println!("{}", formatter.format_refs(&result));
589 }
590 }
591 TraceOperation::Module(module) => {
592 info!(module = %module, "Tracing module");
593 let result = trace_module_cmd(&project, module, &filter).await?;
594 if json_multi_op {
595 combined.module = Some(result);
596 } else if summary_mode {
597 println!(
598 " Exports: {} Imported by: {} Deps: {}",
599 result.exports.len(),
600 result.imported_by.len(),
601 result.dependencies.len()
602 );
603 } else {
604 println!("{}", formatter.format_module(&result));
605 }
606 }
607 TraceOperation::Pattern(pattern) => {
608 info!(pattern = %pattern, "Tracing pattern");
609 let result = trace_pattern_cmd(&project, pattern, &args, &filter).await?;
610 if json_multi_op {
611 combined.pattern = Some(result);
612 } else if summary_mode {
613 println!(
614 " Matches: {} Files: {}",
615 result.total_matches,
616 result.by_file.len()
617 );
618 } else {
619 println!("{}", formatter.format_pattern(&result));
620 }
621 }
622 TraceOperation::Flow(symbol) => {
623 info!(symbol = %symbol, "Tracing data flow");
624 let result = trace_flow_cmd(&project, symbol, &args, &filter).await?;
625 if json_multi_op {
626 combined.flow = Some(result);
627 } else if summary_mode {
628 let total_steps: usize = result.flow_paths.iter().map(|p| p.len()).sum();
629 println!(
630 " Flow paths: {} Steps: {}",
631 result.flow_paths.len(),
632 total_steps
633 );
634 } else {
635 println!("{}", formatter.format_flow(&result));
636 }
637 }
638 TraceOperation::Impact(symbol) => {
639 info!(symbol = %symbol, "Analyzing impact");
640 let result = analyze_impact_cmd(&project, symbol, args.max_depth, &filter).await?;
641 if json_multi_op {
642 combined.impact = Some(result);
643 } else if summary_mode {
644 println!(
645 " Direct callers: {} Transitive: {} Entry points: {} Risk: {:?}",
646 result.direct_callers.len(),
647 result.transitive_callers.len(),
648 result.affected_entry_points.len(),
649 result.risk_level
650 );
651 } else {
652 println!("{}", formatter.format_impact(&result));
653 }
654 }
655 TraceOperation::Scope(location) => {
656 info!(location = %location, "Analyzing scope");
657 let result = analyze_scope_cmd(&project, location, &filter).await?;
658 if json_multi_op {
659 combined.scope = Some(result);
660 } else if summary_mode {
661 println!(
662 " Scope: {} Variables: {} Imports: {}",
663 result.enclosing_scope.as_deref().unwrap_or("global"),
664 result.local_variables.len(),
665 result.imports.len()
666 );
667 } else {
668 println!("{}", formatter.format_scope(&result));
669 }
670 }
671 TraceOperation::DeadCode => {
672 info!("Finding dead code");
673 let result = find_dead_code_cmd(&project, args.limit, &filter, args.xref).await?;
674 if json_multi_op {
675 combined.dead_code = Some(result);
676 } else if args.count || summary_mode {
677 let kinds: Vec<_> = result
678 .by_kind
679 .iter()
680 .map(|(k, v)| format!("{}={}", k, v))
681 .collect();
682 println!(
683 " Dead symbols: {} ({})",
684 result.total_dead,
685 kinds.join(", ")
686 );
687 } else {
688 println!("{}", formatter.format_dead_code(&result));
689 }
690 }
691 TraceOperation::Stats => {
692 info!("Computing statistics");
693 let result = compute_stats_cmd(&project, &filter).await?;
694 if json_multi_op {
695 combined.stats = Some(result);
696 } else if summary_mode {
697 println!(
698 " Files: {} Symbols: {} Refs: {} Edges: {}",
699 result.total_files,
700 result.total_symbols,
701 result.total_references,
702 result.total_edges
703 );
704 } else {
705 println!("{}", formatter.format_stats(&result));
706 }
707 }
708 TraceOperation::Cycles => {
709 info!("Finding circular dependencies");
710 let result = find_cycles_cmd(&project, &filter).await?;
711 if json_multi_op {
712 combined.cycles = Some(result);
713 } else if summary_mode {
714 println!(" Circular deps: {}", result.circular_deps.len());
715 } else {
716 println!("{}", formatter.format_module(&result));
717 }
718 }
719 }
720 }
721
722 if json_multi_op {
724 println!(
725 "{}",
726 serde_json::to_string_pretty(&combined)
727 .unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
728 );
729 }
730
731 Ok(())
732}
733
734fn operation_header(op: &TraceOperation) -> String {
736 match op {
737 TraceOperation::Trace(s) => format!("TRACE: {}", s),
738 TraceOperation::Refs { symbol, kind } => {
739 if let Some(k) = kind {
740 format!("REFERENCES: {} ({:?})", symbol, k)
741 } else {
742 format!("REFERENCES: {}", symbol)
743 }
744 }
745 TraceOperation::Callers(s) => format!("CALLERS: {}", s),
746 TraceOperation::Callees(s) => format!("CALLEES: {}", s),
747 TraceOperation::Type(s) => format!("TYPE USAGE: {}", s),
748 TraceOperation::Module(s) => format!("MODULE: {}", s),
749 TraceOperation::Pattern(s) => format!("PATTERN: {}", s),
750 TraceOperation::Flow(s) => format!("DATA FLOW: {}", s),
751 TraceOperation::Impact(s) => format!("IMPACT ANALYSIS: {}", s),
752 TraceOperation::Scope(s) => format!("SCOPE: {}", s),
753 TraceOperation::DeadCode => "DEAD CODE ANALYSIS".to_string(),
754 TraceOperation::Stats => "CODEBASE STATISTICS".to_string(),
755 TraceOperation::Cycles => "CIRCULAR DEPENDENCIES".to_string(),
756 }
757}
758
759fn load_semantic_index(project: &Project) -> Result<SemanticIndex> {
765 let index_path = trace_index_path(&project.root);
766
767 if !trace_index_exists(&project.root) {
768 return Err(Error::IndexError {
769 message: format!(
770 "Trace index not found. Run 'greppy index' first.\nExpected at: {}",
771 index_path.display()
772 ),
773 });
774 }
775
776 load_index(&index_path).map_err(|e| Error::IndexError {
777 message: format!("Failed to load trace index: {}", e),
778 })
779}
780
781fn get_code_context(cache: &mut FileCache, file: &Path, line: u32, context_lines: u32) -> String {
787 if context_lines == 0 {
788 cache
790 .get_line(file, line)
791 .map(|l| l.trim().to_string())
792 .unwrap_or_else(|| format!("// line {}", line))
793 } else {
794 cache
796 .get_context(file, line, context_lines, context_lines)
797 .map(|ctx| ctx.format(false))
798 .unwrap_or_else(|| format!("// line {}", line))
799 }
800}
801
802async fn find_refs_cmd(
808 project: &Project,
809 symbol: &str,
810 kind_filter: Option<ReferenceKind>,
811 args: &TraceArgs,
812 filter: &TraceFilter,
813) -> Result<RefsResult> {
814 debug!(symbol = %symbol, ?kind_filter, ?filter, "find_refs");
815
816 let index = load_semantic_index(project)?;
817 let mut cache = FileCache::new(&project.root);
818
819 let mut references = Vec::new();
820 let mut by_kind: HashMap<String, usize> = HashMap::new();
821 let mut by_file: HashMap<String, usize> = HashMap::new();
822
823 let symbol_ids = index.symbols_by_name(symbol).cloned().unwrap_or_default();
825
826 let defined_at = symbol_ids.first().and_then(|&id| {
828 let sym = index.symbol(id)?;
829 let file = index.file_path(sym.file_id)?;
830 Some(format!("{}:{}", file.display(), sym.start_line))
831 });
832
833 let symbol_kind = symbol_ids
835 .first()
836 .and_then(|&id| index.symbol(id))
837 .map(|s| symbol_kind_str(s.symbol_kind()).to_string());
838
839 for &sym_id in &symbol_ids {
841 let refs = find_refs(&index, sym_id);
842
843 for ref_ctx in refs {
844 let kind = match ref_ctx.reference.ref_kind() {
846 RefKind::Read => ReferenceKind::Read,
847 RefKind::Write => ReferenceKind::Write,
848 RefKind::Call => ReferenceKind::Call,
849 RefKind::TypeAnnotation => ReferenceKind::TypeAnnotation,
850 RefKind::Import => ReferenceKind::Import,
851 RefKind::Export => ReferenceKind::Export,
852 RefKind::Construction => ReferenceKind::Call, RefKind::Inheritance | RefKind::Decorator | RefKind::Unknown => ReferenceKind::Read,
854 };
855
856 if let Some(filter_kind) = kind_filter {
858 if kind != filter_kind {
859 continue;
860 }
861 }
862
863 let file_path = index
865 .file_path(ref_ctx.file_id)
866 .map(|p| p.to_path_buf())
867 .unwrap_or_default();
868 let file = file_path.to_string_lossy().to_string();
869
870 if !filter.matches_path(&file) {
872 continue;
873 }
874
875 let enclosing_symbol = find_enclosing_symbol(&index, ref_ctx.file_id, ref_ctx.line);
877
878 let context = get_code_context(&mut cache, &file_path, ref_ctx.line, args.context);
880
881 *by_kind
883 .entry(reference_kind_str(kind).to_string())
884 .or_insert(0) += 1;
885 *by_file.entry(file.clone()).or_insert(0) += 1;
886
887 references.push(ReferenceInfo {
888 file,
889 line: ref_ctx.line,
890 column: ref_ctx.column,
891 kind,
892 context,
893 enclosing_symbol,
894 });
895 }
896 }
897
898 if let Some(token_ids) = index.tokens_by_name(symbol) {
900 for &token_id in token_ids {
901 if let Some(token) = index.token(token_id) {
902 let file_path = index
903 .file_path(token.file_id)
904 .map(|p| p.to_path_buf())
905 .unwrap_or_default();
906 let file = file_path.to_string_lossy().to_string();
907
908 let already_have = references
910 .iter()
911 .any(|r| r.file == file && r.line == token.line && r.column == token.column);
912
913 if already_have {
914 continue;
915 }
916
917 if let Some(ref in_path) = args.r#in {
919 if !file.contains(&in_path.to_string_lossy().to_string()) {
920 continue;
921 }
922 }
923
924 let kind = match token.token_kind() {
925 crate::trace::TokenKind::Call => ReferenceKind::Call,
926 _ => ReferenceKind::Read,
927 };
928
929 if let Some(filter_kind) = kind_filter {
931 if kind != filter_kind {
932 continue;
933 }
934 }
935
936 let enclosing_symbol = find_enclosing_symbol(&index, token.file_id, token.line);
937 let context = get_code_context(&mut cache, &file_path, token.line, args.context);
938
939 *by_kind
940 .entry(reference_kind_str(kind).to_string())
941 .or_insert(0) += 1;
942 *by_file.entry(file.clone()).or_insert(0) += 1;
943
944 references.push(ReferenceInfo {
945 file,
946 line: token.line,
947 column: token.column,
948 kind,
949 context,
950 enclosing_symbol,
951 });
952 }
953 }
954 }
955
956 references.sort_by(|a, b| (&a.file, a.line).cmp(&(&b.file, b.line)));
958
959 if let Some(limit) = args.limit {
961 references.truncate(limit);
962 }
963
964 Ok(RefsResult {
965 symbol: symbol.to_string(),
966 defined_at,
967 symbol_kind,
968 total_refs: references.len(),
969 references,
970 by_kind,
971 by_file,
972 })
973}
974
975fn find_enclosing_symbol(index: &SemanticIndex, file_id: u16, line: u32) -> Option<String> {
977 let mut best: Option<(&crate::trace::Symbol, u32)> = None;
978
979 for symbol in &index.symbols {
980 if symbol.file_id == file_id && symbol.start_line <= line && symbol.end_line >= line {
981 let size = symbol.end_line - symbol.start_line;
982 match best {
983 None => best = Some((symbol, size)),
984 Some((_, best_size)) if size < best_size => best = Some((symbol, size)),
985 _ => {}
986 }
987 }
988 }
989
990 best.and_then(|(sym, _)| index.symbol_name(sym).map(|s| s.to_string()))
991}
992
993async fn find_callers_cmd(
999 project: &Project,
1000 symbol: &str,
1001 max_depth: usize,
1002 filter: &TraceFilter,
1003) -> Result<TraceResult> {
1004 let _ = filter; debug!(symbol = %symbol, "find_callers");
1006
1007 let index = load_semantic_index(project)?;
1008
1009 let symbol_ids = index.symbols_by_name(symbol).cloned().unwrap_or_default();
1010 if symbol_ids.is_empty() {
1011 return Ok(TraceResult {
1012 symbol: symbol.to_string(),
1013 defined_at: None,
1014 kind: "unknown".to_string(),
1015 invocation_paths: Vec::new(),
1016 total_paths: 0,
1017 entry_points: 0,
1018 });
1019 }
1020
1021 let mut paths = Vec::new();
1022 let mut visited = HashSet::new();
1023
1024 for &sym_id in &symbol_ids {
1025 collect_callers_recursive(
1026 &index,
1027 sym_id,
1028 &mut paths,
1029 &mut visited,
1030 Vec::new(),
1031 max_depth,
1032 );
1033 }
1034
1035 let defined_at = symbol_ids.first().and_then(|&id| {
1036 let sym = index.symbol(id)?;
1037 let file = index.file_path(sym.file_id)?;
1038 Some(format!("{}:{}", file.display(), sym.start_line))
1039 });
1040
1041 let kind = symbol_ids
1042 .first()
1043 .and_then(|&id| index.symbol(id))
1044 .map(|s| symbol_kind_str(s.symbol_kind()).to_string())
1045 .unwrap_or_else(|| "function".to_string());
1046
1047 let entry_points = paths
1048 .iter()
1049 .map(|p| &p.entry_point)
1050 .collect::<HashSet<_>>()
1051 .len();
1052
1053 Ok(TraceResult {
1054 symbol: symbol.to_string(),
1055 defined_at,
1056 kind,
1057 invocation_paths: paths.clone(),
1058 total_paths: paths.len(),
1059 entry_points,
1060 })
1061}
1062
1063fn collect_callers_recursive(
1064 index: &SemanticIndex,
1065 sym_id: u32,
1066 paths: &mut Vec<InvocationPath>,
1067 visited: &mut HashSet<u32>,
1068 current_chain: Vec<ChainStep>,
1069 max_depth: usize,
1070) {
1071 if current_chain.len() >= max_depth {
1072 return;
1073 }
1074
1075 let callers = index.callers(sym_id);
1076 if callers.is_empty() && !current_chain.is_empty() {
1077 if let Some(sym) = index.symbol(sym_id) {
1079 let name = index.symbol_name(sym).unwrap_or("<unknown>");
1080 let file = index
1081 .file_path(sym.file_id)
1082 .map(|p| p.to_string_lossy().to_string())
1083 .unwrap_or_default();
1084
1085 let mut chain = current_chain.clone();
1086 chain.push(ChainStep {
1087 symbol: name.to_string(),
1088 file: file.clone(),
1089 line: sym.start_line,
1090 column: None,
1091 context: None,
1092 });
1093
1094 paths.push(InvocationPath {
1095 entry_point: format!("{} ({})", name, file),
1096 entry_kind: symbol_kind_str(sym.symbol_kind()).to_string(),
1097 chain,
1098 });
1099 }
1100 return;
1101 }
1102
1103 for &caller_id in callers {
1104 if visited.contains(&caller_id) {
1105 continue;
1106 }
1107 visited.insert(caller_id);
1108
1109 if let Some(caller) = index.symbol(caller_id) {
1110 let name = index.symbol_name(caller).unwrap_or("<unknown>");
1111 let file = index
1112 .file_path(caller.file_id)
1113 .map(|p| p.to_string_lossy().to_string())
1114 .unwrap_or_default();
1115
1116 let call_line = index
1118 .edges
1119 .iter()
1120 .find(|e| e.from_symbol == caller_id && e.to_symbol == sym_id)
1121 .map(|e| e.line)
1122 .unwrap_or(caller.start_line);
1123
1124 let mut new_chain = current_chain.clone();
1125 new_chain.push(ChainStep {
1126 symbol: name.to_string(),
1127 file,
1128 line: call_line,
1129 column: None,
1130 context: None,
1131 });
1132
1133 collect_callers_recursive(index, caller_id, paths, visited, new_chain, max_depth);
1134 }
1135 }
1136}
1137
1138async fn find_callees_cmd(
1140 project: &Project,
1141 symbol: &str,
1142 max_depth: usize,
1143 filter: &TraceFilter,
1144) -> Result<TraceResult> {
1145 let _ = filter; debug!(symbol = %symbol, "find_callees");
1147
1148 let index = load_semantic_index(project)?;
1149
1150 let symbol_ids = index.symbols_by_name(symbol).cloned().unwrap_or_default();
1151 if symbol_ids.is_empty() {
1152 return Ok(TraceResult {
1153 symbol: symbol.to_string(),
1154 defined_at: None,
1155 kind: "unknown".to_string(),
1156 invocation_paths: Vec::new(),
1157 total_paths: 0,
1158 entry_points: 0,
1159 });
1160 }
1161
1162 let mut paths = Vec::new();
1163
1164 for &sym_id in &symbol_ids {
1165 let mut visited = HashSet::new();
1166 collect_callees_recursive(
1167 &index,
1168 sym_id,
1169 &mut paths,
1170 &mut visited,
1171 Vec::new(),
1172 max_depth,
1173 );
1174 }
1175
1176 let defined_at = symbol_ids.first().and_then(|&id| {
1177 let sym = index.symbol(id)?;
1178 let file = index.file_path(sym.file_id)?;
1179 Some(format!("{}:{}", file.display(), sym.start_line))
1180 });
1181
1182 let kind = symbol_ids
1183 .first()
1184 .and_then(|&id| index.symbol(id))
1185 .map(|s| symbol_kind_str(s.symbol_kind()).to_string())
1186 .unwrap_or_else(|| "function".to_string());
1187
1188 Ok(TraceResult {
1189 symbol: symbol.to_string(),
1190 defined_at,
1191 kind,
1192 invocation_paths: paths.clone(),
1193 total_paths: paths.len(),
1194 entry_points: 1,
1195 })
1196}
1197
1198fn collect_callees_recursive(
1199 index: &SemanticIndex,
1200 sym_id: u32,
1201 paths: &mut Vec<InvocationPath>,
1202 visited: &mut HashSet<u32>,
1203 current_chain: Vec<ChainStep>,
1204 max_depth: usize,
1205) {
1206 if current_chain.len() >= max_depth {
1207 return;
1208 }
1209
1210 visited.insert(sym_id);
1211
1212 let callees = index.callees(sym_id);
1213
1214 let mut chain = current_chain.clone();
1216 if let Some(sym) = index.symbol(sym_id) {
1217 let name = index.symbol_name(sym).unwrap_or("<unknown>");
1218 let file = index
1219 .file_path(sym.file_id)
1220 .map(|p| p.to_string_lossy().to_string())
1221 .unwrap_or_default();
1222
1223 chain.push(ChainStep {
1224 symbol: name.to_string(),
1225 file: file.clone(),
1226 line: sym.start_line,
1227 column: None,
1228 context: None,
1229 });
1230
1231 if callees.is_empty() && !chain.is_empty() {
1232 paths.push(InvocationPath {
1234 entry_point: chain.first().map(|c| c.symbol.clone()).unwrap_or_default(),
1235 entry_kind: symbol_kind_str(sym.symbol_kind()).to_string(),
1236 chain: chain.clone(),
1237 });
1238 return;
1239 }
1240 }
1241
1242 for &callee_id in callees {
1243 if visited.contains(&callee_id) {
1244 continue;
1245 }
1246 collect_callees_recursive(index, callee_id, paths, visited, chain.clone(), max_depth);
1247 }
1248}
1249
1250async fn analyze_impact_cmd(
1256 project: &Project,
1257 symbol: &str,
1258 max_depth: usize,
1259 filter: &TraceFilter,
1260) -> Result<ImpactResult> {
1261 let _ = filter; debug!(symbol = %symbol, "analyze_impact");
1263
1264 let index = load_semantic_index(project)?;
1265
1266 let sym_name = if symbol.contains(':') {
1268 symbol.splitn(2, ':').nth(1).unwrap_or(symbol)
1269 } else {
1270 symbol
1271 };
1272
1273 let symbol_ids = index.symbols_by_name(sym_name).cloned().unwrap_or_default();
1274
1275 if symbol_ids.is_empty() {
1276 return Ok(ImpactResult {
1277 symbol: symbol.to_string(),
1278 file: String::new(),
1279 defined_at: None,
1280 direct_callers: Vec::new(),
1281 direct_caller_count: 0,
1282 transitive_callers: Vec::new(),
1283 transitive_caller_count: 0,
1284 affected_entry_points: Vec::new(),
1285 files_affected: Vec::new(),
1286 risk_level: RiskLevel::Low,
1287 });
1288 }
1289
1290 let first_id = symbol_ids[0];
1291 let defined_at = index.symbol(first_id).and_then(|sym| {
1292 let file = index.file_path(sym.file_id)?;
1293 Some(format!("{}:{}", file.display(), sym.start_line))
1294 });
1295
1296 let file = index
1297 .symbol(first_id)
1298 .and_then(|s| index.file_path(s.file_id))
1299 .map(|p| p.to_string_lossy().to_string())
1300 .unwrap_or_default();
1301
1302 let mut direct_callers_set = HashSet::new();
1304 let mut direct_caller_files = HashSet::new();
1305
1306 for &sym_id in &symbol_ids {
1307 for &caller_id in index.callers(sym_id) {
1308 if let Some(caller) = index.symbol(caller_id) {
1309 let name = index.symbol_name(caller).unwrap_or("<unknown>");
1310 let caller_file = index
1311 .file_path(caller.file_id)
1312 .map(|p| p.to_string_lossy().to_string())
1313 .unwrap_or_default();
1314
1315 let caller_str = format!("{} ({}:{})", name, caller_file, caller.start_line);
1316 direct_callers_set.insert(caller_str);
1317 direct_caller_files.insert(caller_file);
1318 }
1319 }
1320 }
1321 let direct_callers: Vec<_> = direct_callers_set.into_iter().collect();
1322
1323 let mut transitive_callers_set = HashSet::new();
1325 let mut visited = HashSet::new();
1326 let mut queue: Vec<(u32, usize)> = symbol_ids.iter().map(|&id| (id, 0)).collect();
1327 let mut affected_entry_points_set = HashSet::new();
1328 let mut all_files = HashSet::new();
1329
1330 while let Some((current, depth)) = queue.pop() {
1331 if depth > max_depth || visited.contains(¤t) {
1332 continue;
1333 }
1334 visited.insert(current);
1335
1336 if let Some(sym) = index.symbol(current) {
1337 let sym_file = index
1338 .file_path(sym.file_id)
1339 .map(|p| p.to_string_lossy().to_string())
1340 .unwrap_or_default();
1341 all_files.insert(sym_file.clone());
1342
1343 if sym.is_entry_point() {
1344 let name = index.symbol_name(sym).unwrap_or("<unknown>");
1345 affected_entry_points_set.insert(format!("{} ({})", name, sym_file));
1346 }
1347
1348 if depth > 1 {
1349 let name = index.symbol_name(sym).unwrap_or("<unknown>");
1350 transitive_callers_set.insert(format!("{} (depth {})", name, depth));
1351 }
1352 }
1353
1354 for &caller_id in index.callers(current) {
1355 if !visited.contains(&caller_id) {
1356 queue.push((caller_id, depth + 1));
1357 }
1358 }
1359 }
1360 let transitive_callers: Vec<_> = transitive_callers_set.into_iter().collect();
1361 let affected_entry_points: Vec<_> = affected_entry_points_set.into_iter().collect();
1362
1363 let risk_level = if affected_entry_points.len() > 10 || all_files.len() > 50 {
1365 RiskLevel::Critical
1366 } else if affected_entry_points.len() > 5 || all_files.len() > 20 {
1367 RiskLevel::High
1368 } else if direct_callers.len() > 5 || all_files.len() > 5 {
1369 RiskLevel::Medium
1370 } else {
1371 RiskLevel::Low
1372 };
1373
1374 Ok(ImpactResult {
1375 symbol: symbol.to_string(),
1376 file,
1377 defined_at,
1378 direct_callers: direct_callers.clone(),
1379 direct_caller_count: direct_callers.len(),
1380 transitive_callers: transitive_callers.clone(),
1381 transitive_caller_count: transitive_callers.len(),
1382 affected_entry_points,
1383 files_affected: all_files.into_iter().collect(),
1384 risk_level,
1385 })
1386}
1387
1388async fn trace_module_cmd(
1400 project: &Project,
1401 module: &str,
1402 filter: &TraceFilter,
1403) -> Result<ModuleResult> {
1404 let _ = filter; debug!(module = %module, "trace_module");
1406
1407 let index = load_semantic_index(project)?;
1408
1409 let module_files: Vec<_> = index
1411 .files
1412 .iter()
1413 .enumerate()
1414 .filter(|(_, path)| path.to_string_lossy().contains(module))
1415 .collect();
1416
1417 let mut exports = Vec::new();
1418 let mut imported_by = Vec::new();
1419 let mut dependencies = Vec::new();
1420
1421 for (file_id, _file_path) in &module_files {
1422 for symbol in index.symbols_in_file(*file_id as u16) {
1424 if symbol.is_exported() {
1425 let name = index.symbol_name(symbol).unwrap_or("<unknown>");
1426 exports.push(format!(
1427 "{} ({})",
1428 name,
1429 symbol_kind_str(symbol.symbol_kind())
1430 ));
1431 }
1432 }
1433
1434 for symbol in index.symbols_in_file(*file_id as u16) {
1437 for reference in index.references_to(symbol.id) {
1438 if reference.ref_kind() == RefKind::Import {
1439 if let Some(token) = index.token(reference.token_id) {
1440 if token.file_id != *file_id as u16 {
1441 let importer_file = index
1442 .file_path(token.file_id)
1443 .map(|p| p.to_string_lossy().to_string())
1444 .unwrap_or_default();
1445 let name = index.symbol_name(symbol).unwrap_or("<unknown>");
1446 imported_by.push(format!("{} imports {}", importer_file, name));
1447 }
1448 }
1449 }
1450 }
1451 }
1452 }
1453
1454 for (file_id, _) in &module_files {
1456 for token in index.tokens_in_file(*file_id as u16) {
1457 if token.token_kind() == crate::trace::TokenKind::Import {
1458 let name = index.token_name(token).unwrap_or("<unknown>");
1459 if !dependencies.contains(&name.to_string()) {
1460 dependencies.push(name.to_string());
1461 }
1462 }
1463 }
1464 }
1465
1466 let file_path = module_files
1467 .first()
1468 .map(|(_, p)| p.to_string_lossy().to_string())
1469 .unwrap_or_else(|| module.to_string());
1470
1471 Ok(ModuleResult {
1472 module: module.to_string(),
1473 file_path,
1474 exports,
1475 imported_by,
1476 dependencies,
1477 circular_deps: Vec::new(), })
1479}
1480
1481async fn find_cycles_cmd(project: &Project, filter: &TraceFilter) -> Result<ModuleResult> {
1483 debug!("find_cycles filter={:?}", filter);
1484
1485 let index = load_semantic_index(project)?;
1486
1487 let file_passes = |file_id: u16| -> bool {
1489 if let Some(path) = index.file_path(file_id) {
1490 filter.matches_path(&path.to_string_lossy())
1491 } else {
1492 false
1493 }
1494 };
1495
1496 let mut file_deps: HashMap<u16, HashSet<u16>> = HashMap::new();
1498
1499 for edge in &index.edges {
1500 if let (Some(from_sym), Some(to_sym)) =
1501 (index.symbol(edge.from_symbol), index.symbol(edge.to_symbol))
1502 {
1503 if from_sym.file_id != to_sym.file_id {
1504 let from_passes = file_passes(from_sym.file_id);
1506 let to_passes = file_passes(to_sym.file_id);
1507
1508 if from_passes || to_passes || filter.path.is_none() {
1509 file_deps
1510 .entry(from_sym.file_id)
1511 .or_default()
1512 .insert(to_sym.file_id);
1513 }
1514 }
1515 }
1516 }
1517
1518 let mut cycles = Vec::new();
1520 let mut visited = HashSet::new();
1521 let mut rec_stack = HashSet::new();
1522 let mut path = Vec::new();
1523
1524 for &file_id in file_deps.keys() {
1525 find_cycles_dfs(
1526 file_id,
1527 &file_deps,
1528 &mut visited,
1529 &mut rec_stack,
1530 &mut path,
1531 &mut cycles,
1532 &index,
1533 filter,
1534 );
1535 }
1536
1537 Ok(ModuleResult {
1538 module: "Circular Dependencies".to_string(),
1539 file_path: String::new(),
1540 exports: Vec::new(),
1541 imported_by: Vec::new(),
1542 dependencies: Vec::new(),
1543 circular_deps: cycles,
1544 })
1545}
1546
1547fn find_cycles_dfs(
1548 node: u16,
1549 graph: &HashMap<u16, HashSet<u16>>,
1550 visited: &mut HashSet<u16>,
1551 rec_stack: &mut HashSet<u16>,
1552 path: &mut Vec<u16>,
1553 cycles: &mut Vec<String>,
1554 index: &SemanticIndex,
1555 filter: &TraceFilter,
1556) {
1557 visited.insert(node);
1558 rec_stack.insert(node);
1559 path.push(node);
1560
1561 if let Some(neighbors) = graph.get(&node) {
1562 for &neighbor in neighbors {
1563 if !visited.contains(&neighbor) {
1564 find_cycles_dfs(
1565 neighbor, graph, visited, rec_stack, path, cycles, index, filter,
1566 );
1567 } else if rec_stack.contains(&neighbor) {
1568 let cycle_start = path.iter().position(|&n| n == neighbor).unwrap_or(0);
1570 let cycle_path: Vec<_> = path[cycle_start..]
1571 .iter()
1572 .filter_map(|&fid| {
1573 index
1574 .file_path(fid)
1575 .map(|p| p.to_string_lossy().to_string())
1576 })
1577 .collect();
1578
1579 if !cycle_path.is_empty() {
1580 let cycle_passes =
1582 filter.path.is_none() || cycle_path.iter().any(|p| filter.matches_path(p));
1583
1584 if cycle_passes {
1585 cycles.push(cycle_path.join(" -> ") + " -> " + &cycle_path[0]);
1586 }
1587 }
1588 }
1589 }
1590 }
1591
1592 path.pop();
1593 rec_stack.remove(&node);
1594}
1595
1596async fn trace_flow_cmd(
1602 project: &Project,
1603 symbol: &str,
1604 args: &TraceArgs,
1605 filter: &TraceFilter,
1606) -> Result<FlowResult> {
1607 debug!(symbol = %symbol, "trace_flow filter={:?}", filter);
1608
1609 let index = load_semantic_index(project)?;
1610 let mut cache = FileCache::new(&project.root);
1611
1612 let mut flow_paths = Vec::new();
1613 let mut current_path = Vec::new();
1614
1615 if let Some(token_ids) = index.tokens_by_name(symbol) {
1617 let mut by_file: HashMap<u16, Vec<&crate::trace::Token>> = HashMap::new();
1619 for &token_id in token_ids {
1620 if let Some(token) = index.token(token_id) {
1621 if let Some(path) = index.file_path(token.file_id) {
1623 if !filter.matches_path(&path.to_string_lossy()) {
1624 continue;
1625 }
1626 }
1627 by_file.entry(token.file_id).or_default().push(token);
1628 }
1629 }
1630
1631 for (file_id, mut tokens) in by_file {
1632 tokens.sort_by_key(|t| (t.line, t.column));
1633
1634 let file_path = index
1635 .file_path(file_id)
1636 .map(|p| p.to_path_buf())
1637 .unwrap_or_default();
1638 let file = file_path.to_string_lossy().to_string();
1639
1640 for token in tokens {
1641 let action = match token.token_kind() {
1644 crate::trace::TokenKind::Call => FlowAction::PassToFunction,
1645 crate::trace::TokenKind::Property => FlowAction::Read,
1646 crate::trace::TokenKind::Identifier => FlowAction::Read, _ => FlowAction::Read,
1648 };
1649
1650 let actual_action = if current_path.is_empty() {
1652 FlowAction::Define
1653 } else {
1654 action
1655 };
1656
1657 let expression = get_code_context(&mut cache, &file_path, token.line, 0);
1658
1659 current_path.push(FlowStep {
1660 variable: symbol.to_string(),
1661 action: actual_action,
1662 file: file.clone(),
1663 line: token.line,
1664 expression,
1665 });
1666 }
1667 }
1668 }
1669
1670 if !current_path.is_empty() {
1671 flow_paths.push(current_path);
1672 }
1673
1674 if let Some(limit) = args.limit {
1676 for path in &mut flow_paths {
1677 path.truncate(limit);
1678 }
1679 }
1680
1681 Ok(FlowResult {
1682 symbol: symbol.to_string(),
1683 flow_paths,
1684 })
1685}
1686
1687async fn trace_pattern_cmd(
1693 project: &Project,
1694 pattern: &str,
1695 args: &TraceArgs,
1696 filter: &TraceFilter,
1697) -> Result<PatternResult> {
1698 debug!(pattern = %pattern, "trace_pattern filter={:?}", filter);
1699
1700 let regex = Regex::new(pattern).map_err(|e| Error::SearchError {
1701 message: format!("Invalid regex pattern: {}", e),
1702 })?;
1703
1704 let index = load_semantic_index(project)?;
1705 let mut cache = FileCache::new(&project.root);
1706
1707 let mut matches = Vec::new();
1708
1709 for (file_id, file_path) in index.files.iter().enumerate() {
1710 let file_str = file_path.to_string_lossy();
1712 if !filter.matches_path(&file_str) {
1713 continue;
1714 }
1715
1716 if let Some(line_count) = cache.line_count(file_path) {
1718 for line_num in 1..=line_count as u32 {
1719 if let Some(line_content) = cache.get_line(file_path, line_num) {
1720 if let Some(mat) = regex.find(&line_content) {
1721 let context = if args.context > 0 {
1722 cache
1723 .get_context(file_path, line_num, args.context, args.context)
1724 .map(|ctx| ctx.format(false))
1725 .unwrap_or_else(|| line_content.clone())
1726 } else {
1727 line_content.trim().to_string()
1728 };
1729
1730 let enclosing = find_enclosing_symbol(&index, file_id as u16, line_num);
1731
1732 matches.push(PatternMatch {
1733 file: file_path.to_string_lossy().to_string(),
1734 line: line_num,
1735 column: mat.start() as u16,
1736 matched_text: mat.as_str().to_string(),
1737 context,
1738 enclosing_symbol: enclosing,
1739 });
1740
1741 if let Some(limit) = args.limit {
1743 if matches.len() >= limit {
1744 break;
1745 }
1746 }
1747 }
1748 }
1749 }
1750 }
1751
1752 if let Some(limit) = args.limit {
1754 if matches.len() >= limit {
1755 break;
1756 }
1757 }
1758 }
1759
1760 let mut by_file: HashMap<String, usize> = HashMap::new();
1762 for m in &matches {
1763 *by_file.entry(m.file.clone()).or_insert(0) += 1;
1764 }
1765
1766 Ok(PatternResult {
1767 pattern: pattern.to_string(),
1768 total_matches: matches.len(),
1769 matches,
1770 by_file,
1771 })
1772}
1773
1774async fn analyze_scope_cmd(
1780 project: &Project,
1781 location: &str,
1782 filter: &TraceFilter,
1783) -> Result<ScopeResult> {
1784 debug!(location = %location, "analyze_scope");
1785 let _ = filter; let parts: Vec<&str> = location.rsplitn(2, ':').collect();
1789 if parts.len() != 2 {
1790 return Err(Error::SearchError {
1791 message: "Location must be in format file:line".to_string(),
1792 });
1793 }
1794
1795 let line: u32 = parts[0].parse().map_err(|_| Error::SearchError {
1796 message: "Invalid line number".to_string(),
1797 })?;
1798 let file_pattern = parts[1];
1799
1800 let index = load_semantic_index(project)?;
1801
1802 let file_id = index
1804 .files
1805 .iter()
1806 .enumerate()
1807 .find(|(_, p)| p.to_string_lossy().contains(file_pattern))
1808 .map(|(id, _)| id as u16);
1809
1810 let file_id = match file_id {
1811 Some(id) => id,
1812 None => {
1813 return Err(Error::SearchError {
1814 message: format!("File not found: {}", file_pattern),
1815 });
1816 }
1817 };
1818
1819 let file_path = index
1820 .file_path(file_id)
1821 .map(|p| p.to_string_lossy().to_string())
1822 .unwrap_or_default();
1823
1824 let enclosing_scope = find_enclosing_symbol(&index, file_id, line);
1826
1827 let mut local_variables = Vec::new();
1829 let mut parameters = Vec::new();
1830 let mut imports = Vec::new();
1831
1832 for symbol in index.symbols_in_file(file_id) {
1833 let name = index.symbol_name(symbol).unwrap_or("<unknown>");
1834 let kind = symbol_kind_str(symbol.symbol_kind());
1835
1836 if symbol.start_line <= line && symbol.end_line >= line {
1838 continue;
1840 }
1841
1842 if symbol.start_line < line && symbol.end_line < line {
1843 match symbol.symbol_kind() {
1845 SymbolKind::Variable | SymbolKind::Constant => {
1846 local_variables.push(ScopeVariable {
1847 name: name.to_string(),
1848 kind: kind.to_string(),
1849 defined_at: symbol.start_line,
1850 });
1851 }
1852 SymbolKind::Function | SymbolKind::Method => {
1853 if let Some(ref scope) = enclosing_scope {
1855 if name != scope {
1856 parameters.push(ScopeVariable {
1857 name: name.to_string(),
1858 kind: kind.to_string(),
1859 defined_at: symbol.start_line,
1860 });
1861 }
1862 }
1863 }
1864 _ => {}
1865 }
1866 }
1867 }
1868
1869 for token in index.tokens_in_file(file_id) {
1871 if token.token_kind() == crate::trace::TokenKind::Import {
1872 let name = index.token_name(token).unwrap_or("<unknown>");
1873 imports.push(name.to_string());
1874 }
1875 }
1876
1877 Ok(ScopeResult {
1878 file: file_path,
1879 line,
1880 enclosing_scope,
1881 local_variables,
1882 parameters,
1883 imports,
1884 })
1885}
1886
1887async fn compute_stats_cmd(project: &Project, filter: &TraceFilter) -> Result<StatsResult> {
1893 debug!("compute_stats");
1894
1895 let index = load_semantic_index(project)?;
1896 let stats = index.stats();
1897
1898 let file_passes = |file_id: u16| -> bool {
1900 if let Some(path) = index.file_path(file_id) {
1901 filter.matches_path(&path.to_string_lossy())
1902 } else {
1903 false
1904 }
1905 };
1906
1907 let symbol_passes = |symbol: &crate::trace::Symbol| -> bool {
1909 if let Some(path) = index.file_path(symbol.file_id) {
1910 let name = index.symbol_name(symbol).unwrap_or("");
1911 let kind = symbol_kind_str(symbol.symbol_kind());
1912 filter.matches_symbol(name, kind, &path.to_string_lossy())
1913 } else {
1914 false
1915 }
1916 };
1917
1918 let mut symbols_by_kind: HashMap<String, usize> = HashMap::new();
1920 let mut filtered_symbol_count = 0;
1921 for symbol in &index.symbols {
1922 if symbol_passes(symbol) {
1923 filtered_symbol_count += 1;
1924 *symbols_by_kind
1925 .entry(symbol_kind_str(symbol.symbol_kind()).to_string())
1926 .or_insert(0) += 1;
1927 }
1928 }
1929
1930 let mut files_by_extension: HashMap<String, usize> = HashMap::new();
1932 let mut filtered_file_count = 0;
1933 for (idx, file) in index.files.iter().enumerate() {
1934 if file_passes(idx as u16) {
1935 filtered_file_count += 1;
1936 let ext = file
1937 .extension()
1938 .map(|e| e.to_string_lossy().to_string())
1939 .unwrap_or_else(|| "unknown".to_string());
1940 *files_by_extension.entry(ext).or_insert(0) += 1;
1941 }
1942 }
1943
1944 let mut symbol_ref_counts: HashMap<String, usize> = HashMap::new();
1946 for s in &index.symbols {
1947 if symbol_passes(s) {
1948 if let Some(name) = index.symbol_name(s) {
1949 let ref_count = index.references_to(s.id).count();
1950 if ref_count > 0 {
1951 *symbol_ref_counts.entry(name.to_string()).or_insert(0) += ref_count;
1952 }
1953 }
1954 }
1955 }
1956 let mut sorted_refs: Vec<_> = symbol_ref_counts.into_iter().collect();
1957 sorted_refs.sort_by(|a, b| b.1.cmp(&a.1));
1958 let most_referenced: Vec<_> = sorted_refs.into_iter().take(10).collect();
1959
1960 let mut file_symbol_counts: HashMap<u16, usize> = HashMap::new();
1962 for symbol in &index.symbols {
1963 if file_passes(symbol.file_id) {
1964 *file_symbol_counts.entry(symbol.file_id).or_insert(0) += 1;
1965 }
1966 }
1967 let mut largest_files: Vec<_> = file_symbol_counts
1968 .into_iter()
1969 .filter_map(|(file_id, count)| {
1970 let path = index.file_path(file_id)?;
1971 Some((path.to_string_lossy().to_string(), count))
1972 })
1973 .collect();
1974 largest_files.sort_by(|a, b| b.1.cmp(&a.1));
1975 largest_files.truncate(10);
1976
1977 let (total_files, total_symbols) =
1979 if filter.path.is_some() || filter.symbol_type.is_some() || filter.name_pattern.is_some() {
1980 (filtered_file_count, filtered_symbol_count)
1981 } else {
1982 (stats.files, stats.symbols)
1983 };
1984
1985 let max_call_depth = calculate_max_call_depth(&index);
1987 let avg_call_depth = calculate_avg_call_depth(&index);
1988
1989 Ok(StatsResult {
1990 total_files,
1991 total_symbols,
1992 total_tokens: stats.tokens, total_references: stats.references, total_edges: stats.edges, total_entry_points: stats.entry_points,
1996 symbols_by_kind,
1997 files_by_extension,
1998 most_referenced,
1999 largest_files,
2000 max_call_depth,
2001 avg_call_depth,
2002 })
2003}
2004
2005fn calculate_max_call_depth(index: &SemanticIndex) -> usize {
2006 let mut max_depth = 0;
2007
2008 for &entry_id in &index.entry_points {
2009 let depth = calculate_depth_from(index, entry_id, &mut HashSet::new());
2010 max_depth = max_depth.max(depth);
2011 }
2012
2013 max_depth
2014}
2015
2016fn calculate_depth_from(index: &SemanticIndex, sym_id: u32, visited: &mut HashSet<u32>) -> usize {
2017 if visited.contains(&sym_id) {
2018 return 0;
2019 }
2020 visited.insert(sym_id);
2021
2022 let callees = index.callees(sym_id);
2023 if callees.is_empty() {
2024 return 0;
2025 }
2026
2027 let max_child_depth = callees
2028 .iter()
2029 .map(|&callee| calculate_depth_from(index, callee, visited))
2030 .max()
2031 .unwrap_or(0);
2032
2033 max_child_depth + 1
2034}
2035
2036fn calculate_avg_call_depth(index: &SemanticIndex) -> f32 {
2037 if index.entry_points.is_empty() {
2038 return 0.0;
2039 }
2040
2041 let total_depth: usize = index
2042 .entry_points
2043 .iter()
2044 .map(|&id| calculate_depth_from(index, id, &mut HashSet::new()))
2045 .sum();
2046
2047 total_depth as f32 / index.entry_points.len() as f32
2048}
2049
2050async fn trace_symbol_cmd(
2056 project: &Project,
2057 symbol: &str,
2058 max_depth: usize,
2059 direct: bool,
2060 filter: &TraceFilter,
2061) -> Result<TraceResult> {
2062 debug!(symbol = %symbol, max_depth, direct, ?filter, "trace_symbol");
2063
2064 let index = load_semantic_index(project)?;
2065
2066 let symbols_to_search = if direct {
2068 vec![symbol.to_string()]
2069 } else {
2070 expand_query_with_ai(symbol).await
2071 };
2072
2073 debug!(symbols = ?symbols_to_search, "Searching for symbols");
2074
2075 let mut all_trace_results = Vec::new();
2077 for sym_name in &symbols_to_search {
2078 let trace_results = trace_symbol_by_name(&index, sym_name, Some(max_depth));
2079 all_trace_results.extend(trace_results);
2080 }
2081
2082 if all_trace_results.is_empty() {
2083 return Ok(TraceResult {
2084 symbol: symbol.to_string(),
2085 defined_at: None,
2086 kind: "unknown".to_string(),
2087 invocation_paths: Vec::new(),
2088 total_paths: 0,
2089 entry_points: 0,
2090 });
2091 }
2092
2093 let mut invocation_paths = Vec::new();
2095 let mut entry_points_set = std::collections::HashSet::new();
2096
2097 for trace_result in &all_trace_results {
2098 for path in &trace_result.paths {
2099 let entry_symbol = index.symbol(path.entry_point);
2100 let entry_name = entry_symbol
2101 .and_then(|s| index.symbol_name(s))
2102 .unwrap_or("<unknown>");
2103 let entry_file = entry_symbol
2104 .and_then(|s| index.file_path(s.file_id))
2105 .map(|p| p.to_string_lossy().to_string())
2106 .unwrap_or_default();
2107 let entry_kind = entry_symbol
2108 .map(|s| symbol_kind_str(s.symbol_kind()))
2109 .unwrap_or("function");
2110
2111 entry_points_set.insert(path.entry_point);
2112
2113 let chain: Vec<ChainStep> = path
2114 .chain
2115 .iter()
2116 .enumerate()
2117 .filter_map(|(i, &sym_id)| {
2118 let sym = index.symbol(sym_id)?;
2119 let name = index.symbol_name(sym)?;
2120 let file = index
2121 .file_path(sym.file_id)
2122 .map(|p| p.to_string_lossy().to_string())
2123 .unwrap_or_default();
2124 let line = if i > 0 {
2125 path.call_lines
2126 .get(i - 1)
2127 .copied()
2128 .unwrap_or(sym.start_line)
2129 } else {
2130 sym.start_line
2131 };
2132
2133 Some(ChainStep {
2134 symbol: name.to_string(),
2135 file,
2136 line,
2137 column: None,
2138 context: None,
2139 })
2140 })
2141 .collect();
2142
2143 if !chain.is_empty() {
2144 invocation_paths.push(InvocationPath {
2145 entry_point: format!("{} ({})", entry_name, entry_file),
2146 entry_kind: entry_kind.to_string(),
2147 chain,
2148 });
2149 }
2150 }
2151 }
2152
2153 if !direct && invocation_paths.len() > 1 {
2155 invocation_paths = rerank_paths_with_ai(symbol, invocation_paths).await;
2156 }
2157
2158 let first_target = all_trace_results.first().map(|r| r.target);
2159 let defined_at = first_target.and_then(|id| {
2160 let sym = index.symbol(id)?;
2161 let file = index.file_path(sym.file_id)?;
2162 Some(format!("{}:{}", file.display(), sym.start_line))
2163 });
2164 let kind = first_target
2165 .and_then(|id| index.symbol(id))
2166 .map(|s| symbol_kind_str(s.symbol_kind()).to_string())
2167 .unwrap_or_else(|| "function".to_string());
2168
2169 Ok(TraceResult {
2170 symbol: symbol.to_string(),
2171 defined_at,
2172 kind,
2173 invocation_paths: invocation_paths.clone(),
2174 total_paths: invocation_paths.len(),
2175 entry_points: entry_points_set.len(),
2176 })
2177}
2178
2179async fn expand_query_with_ai(query: &str) -> Vec<String> {
2185 let providers = auth::get_authenticated_providers();
2186
2187 if providers.is_empty() {
2188 debug!("No AI provider authenticated, skipping query expansion");
2189 return vec![query.to_string()];
2190 }
2191
2192 if !is_natural_language_query(query) && query.len() > 15 {
2193 return vec![query.to_string()];
2194 }
2195
2196 let expanded = if providers.contains(&Provider::Anthropic) {
2197 match auth::get_anthropic_token() {
2198 Ok(token) => {
2199 let client = ClaudeClient::new(token);
2200 match client.expand_query(query).await {
2201 Ok(symbols) => {
2202 debug!(count = symbols.len(), "AI expanded query to symbols");
2203 symbols
2204 }
2205 Err(e) => {
2206 warn!("AI query expansion failed: {}", e);
2207 vec![query.to_string()]
2208 }
2209 }
2210 }
2211 Err(e) => {
2212 warn!("Failed to get Anthropic token: {}", e);
2213 vec![query.to_string()]
2214 }
2215 }
2216 } else if providers.contains(&Provider::Google) {
2217 match auth::get_google_token() {
2218 Ok(token) => {
2219 let client = GeminiClient::new(token);
2220 match client.expand_query(query).await {
2221 Ok(symbols) => {
2222 debug!(count = symbols.len(), "AI expanded query to symbols");
2223 symbols
2224 }
2225 Err(e) => {
2226 warn!("AI query expansion failed: {}", e);
2227 vec![query.to_string()]
2228 }
2229 }
2230 }
2231 Err(e) => {
2232 warn!("Failed to get Google token: {}", e);
2233 vec![query.to_string()]
2234 }
2235 }
2236 } else {
2237 vec![query.to_string()]
2238 };
2239
2240 let mut result = expanded;
2241 if !result.iter().any(|s| s.eq_ignore_ascii_case(query)) {
2242 result.insert(0, query.to_string());
2243 }
2244 result.truncate(10);
2245 result
2246}
2247
2248async fn rerank_paths_with_ai(query: &str, mut paths: Vec<InvocationPath>) -> Vec<InvocationPath> {
2250 let providers = auth::get_authenticated_providers();
2251
2252 if providers.is_empty() {
2253 debug!("No AI provider authenticated, skipping reranking");
2254 return paths;
2255 }
2256
2257 if paths.len() <= 3 {
2258 return paths;
2259 }
2260
2261 let path_descriptions: Vec<String> = paths
2262 .iter()
2263 .map(|p| {
2264 let chain_str: Vec<String> = p.chain.iter().map(|c| c.symbol.clone()).collect();
2265 format!(
2266 "Entry: {} ({})\nChain: {}",
2267 p.entry_point,
2268 p.entry_kind,
2269 chain_str.join(" -> ")
2270 )
2271 })
2272 .collect();
2273
2274 let indices = if providers.contains(&Provider::Anthropic) {
2275 match auth::get_anthropic_token() {
2276 Ok(token) => {
2277 let client = ClaudeClient::new(token);
2278 match client.rerank_trace(query, &path_descriptions).await {
2279 Ok(idx) => {
2280 debug!(order = ?idx, "AI reranked trace paths");
2281 idx
2282 }
2283 Err(e) => {
2284 warn!("AI reranking failed: {}", e);
2285 (0..paths.len()).collect()
2286 }
2287 }
2288 }
2289 Err(e) => {
2290 warn!("Failed to get Anthropic token: {}", e);
2291 (0..paths.len()).collect()
2292 }
2293 }
2294 } else if providers.contains(&Provider::Google) {
2295 match auth::get_google_token() {
2296 Ok(token) => {
2297 let client = GeminiClient::new(token);
2298 match client.rerank_trace(query, &path_descriptions).await {
2299 Ok(idx) => {
2300 debug!(order = ?idx, "AI reranked trace paths");
2301 idx
2302 }
2303 Err(e) => {
2304 warn!("AI reranking failed: {}", e);
2305 (0..paths.len()).collect()
2306 }
2307 }
2308 }
2309 Err(e) => {
2310 warn!("Failed to get Google token: {}", e);
2311 (0..paths.len()).collect()
2312 }
2313 }
2314 } else {
2315 (0..paths.len()).collect()
2316 };
2317
2318 let original_paths = std::mem::take(&mut paths);
2319 let mut reranked = Vec::with_capacity(original_paths.len());
2320
2321 for &idx in &indices {
2322 if idx < original_paths.len() {
2323 reranked.push(original_paths[idx].clone());
2324 }
2325 }
2326
2327 for (i, path) in original_paths.into_iter().enumerate() {
2328 if !indices.contains(&i) {
2329 reranked.push(path);
2330 }
2331 }
2332
2333 reranked
2334}
2335
2336async fn find_dead_code_cmd(
2342 project: &Project,
2343 limit: Option<usize>,
2344 filter: &TraceFilter,
2345 xref: bool,
2346) -> Result<DeadCodeResult> {
2347 debug!("find_dead_code filter={:?} xref={}", filter, xref);
2348
2349 let index = load_semantic_index(project)?;
2350
2351 let dead_symbols = find_dead_symbols(&index);
2352
2353 let mut symbols = Vec::new();
2354 let mut by_kind: HashMap<String, usize> = HashMap::new();
2355 let mut by_file: HashMap<String, usize> = HashMap::new();
2356
2357 for sym in dead_symbols {
2358 let file = index
2359 .file_path(sym.file_id)
2360 .map(|p| p.to_string_lossy().to_string())
2361 .unwrap_or_else(|| "<unknown>".to_string());
2362
2363 let name = index.symbol_name(sym).unwrap_or("<unknown>").to_string();
2364 let kind = symbol_kind_str(sym.symbol_kind()).to_string();
2365
2366 if !filter.matches_symbol(&name, &kind, &file) {
2368 continue;
2369 }
2370
2371 *by_kind.entry(kind.clone()).or_insert(0) += 1;
2372 *by_file.entry(file.clone()).or_insert(0) += 1;
2373
2374 let potential_callers = if xref {
2376 find_potential_callers(&index, sym, &name)
2377 } else {
2378 Vec::new()
2379 };
2380
2381 symbols.push(DeadSymbol {
2382 name,
2383 kind,
2384 file,
2385 line: sym.start_line,
2386 reason: "No references or calls found".to_string(),
2387 potential_callers,
2388 });
2389 }
2390
2391 symbols.sort_by(|a, b| (&a.file, a.line).cmp(&(&b.file, b.line)));
2393
2394 if let Some(limit) = limit {
2396 symbols.truncate(limit);
2397 }
2398
2399 Ok(DeadCodeResult {
2400 total_dead: symbols.len(),
2401 symbols,
2402 by_kind,
2403 by_file,
2404 })
2405}
2406
2407fn find_potential_callers(
2409 index: &SemanticIndex,
2410 dead_sym: &crate::trace::Symbol,
2411 dead_name: &str,
2412) -> Vec<PotentialCaller> {
2413 let mut callers = Vec::new();
2414
2415 let same_file_symbols: Vec<_> = index
2417 .symbols
2418 .iter()
2419 .filter(|s| {
2420 s.file_id == dead_sym.file_id
2421 && s.id != dead_sym.id
2422 && matches!(s.symbol_kind(), SymbolKind::Function | SymbolKind::Method)
2423 })
2424 .collect();
2425
2426 for caller in same_file_symbols.iter().take(3) {
2427 if let Some(caller_name) = index.symbol_name(caller) {
2428 if let Some(path) = index.file_path(caller.file_id) {
2429 callers.push(PotentialCaller {
2430 name: caller_name.to_string(),
2431 file: path.to_string_lossy().to_string(),
2432 line: caller.start_line,
2433 reason: "Same file - could call this".to_string(),
2434 });
2435 }
2436 }
2437 }
2438
2439 for &entry_id in index.entry_points.iter().take(2) {
2441 if let Some(entry_sym) = index.symbols.iter().find(|s| s.id == entry_id) {
2442 if entry_sym.id != dead_sym.id {
2443 if let Some(entry_name) = index.symbol_name(entry_sym) {
2444 if let Some(path) = index.file_path(entry_sym.file_id) {
2445 callers.push(PotentialCaller {
2446 name: entry_name.to_string(),
2447 file: path.to_string_lossy().to_string(),
2448 line: entry_sym.start_line,
2449 reason: "Entry point - could reach this".to_string(),
2450 });
2451 }
2452 }
2453 }
2454 }
2455 }
2456
2457 if let Some(token_ids) = index.tokens_by_name(dead_name) {
2459 for &token_id in token_ids.iter().take(2) {
2460 if let Some(token) = index.token(token_id) {
2461 if token.file_id != dead_sym.file_id || token.line != dead_sym.start_line {
2463 if let Some(path) = index.file_path(token.file_id) {
2464 callers.push(PotentialCaller {
2465 name: dead_name.to_string(),
2466 file: path.to_string_lossy().to_string(),
2467 line: token.line,
2468 reason: "Token match - name appears here".to_string(),
2469 });
2470 }
2471 }
2472 }
2473 }
2474 }
2475
2476 callers
2477}
2478
2479async fn run_tui(_args: &TraceArgs, _project: &Project) -> Result<()> {
2481 eprintln!("TUI mode is not yet implemented.");
2482 eprintln!("Use --json or default ASCII output instead.");
2483 Err(Error::SearchError {
2484 message: "TUI mode not implemented".to_string(),
2485 })
2486}
2487
2488#[cfg(test)]
2489mod tests {
2490 use super::*;
2491
2492 #[test]
2493 fn test_args_output_format() {
2494 let args = TraceArgs {
2495 symbol: Some("test".to_string()),
2496 direct: false,
2497 refs: None,
2498 reads: None,
2499 writes: None,
2500 callers: None,
2501 callees: None,
2502 type_name: None,
2503 module: None,
2504 pattern: None,
2505 flow: None,
2506 impact: None,
2507 scope: None,
2508 dead: false,
2509 stats: false,
2510 cycles: false,
2511 kind: None,
2512 r#in: None,
2513 symbol_type: None,
2514 name: None,
2515 group_by: None,
2516 json: true,
2517 plain: false,
2518 csv: false,
2519 dot: false,
2520 markdown: false,
2521 tui: false,
2522 max_depth: 10,
2523 context: 0,
2524 limit: None,
2525 count: false,
2526 summary: false,
2527 xref: false,
2528 project: None,
2529 };
2530 assert_eq!(args.output_format(), OutputFormat::Json);
2531 }
2532
2533 #[test]
2534 fn test_args_operations() {
2535 let args = TraceArgs {
2536 symbol: Some("test".to_string()),
2537 direct: false,
2538 refs: None,
2539 reads: None,
2540 writes: None,
2541 callers: None,
2542 callees: None,
2543 type_name: None,
2544 module: None,
2545 pattern: None,
2546 flow: None,
2547 impact: None,
2548 scope: None,
2549 dead: false,
2550 stats: false,
2551 cycles: false,
2552 kind: None,
2553 r#in: None,
2554 symbol_type: None,
2555 name: None,
2556 group_by: None,
2557 json: false,
2558 plain: false,
2559 csv: false,
2560 dot: false,
2561 markdown: false,
2562 tui: false,
2563 max_depth: 10,
2564 context: 0,
2565 limit: None,
2566 count: false,
2567 summary: false,
2568 xref: false,
2569 project: None,
2570 };
2571
2572 let ops = args.operations();
2573 assert_eq!(ops.len(), 1);
2574 match &ops[0] {
2575 TraceOperation::Trace(sym) => assert_eq!(sym, "test"),
2576 _ => panic!("Expected Trace operation"),
2577 }
2578 }
2579
2580 #[test]
2581 fn test_args_refs_operations() {
2582 let args = TraceArgs {
2583 symbol: None,
2584 direct: false,
2585 refs: Some("userId".to_string()),
2586 reads: None,
2587 writes: None,
2588 callers: None,
2589 callees: None,
2590 type_name: None,
2591 module: None,
2592 pattern: None,
2593 flow: None,
2594 impact: None,
2595 scope: None,
2596 dead: false,
2597 stats: false,
2598 cycles: false,
2599 kind: None,
2600 r#in: None,
2601 symbol_type: None,
2602 name: None,
2603 group_by: None,
2604 json: false,
2605 plain: false,
2606 csv: false,
2607 dot: false,
2608 markdown: false,
2609 tui: false,
2610 max_depth: 10,
2611 context: 0,
2612 limit: None,
2613 count: false,
2614 summary: false,
2615 xref: false,
2616 project: None,
2617 };
2618
2619 let ops = args.operations();
2620 assert_eq!(ops.len(), 1);
2621 match &ops[0] {
2622 TraceOperation::Refs { symbol, kind } => {
2623 assert_eq!(symbol, "userId");
2624 assert!(kind.is_none());
2625 }
2626 _ => panic!("Expected Refs operation"),
2627 }
2628 }
2629
2630 #[test]
2631 fn test_args_composable_operations() {
2632 let args = TraceArgs {
2634 symbol: Some("test".to_string()),
2635 direct: false,
2636 refs: Some("other".to_string()),
2637 reads: None,
2638 writes: None,
2639 callers: None,
2640 callees: None,
2641 type_name: None,
2642 module: None,
2643 pattern: None,
2644 flow: None,
2645 impact: None,
2646 scope: None,
2647 dead: true,
2648 stats: true,
2649 cycles: false,
2650 kind: None,
2651 r#in: None,
2652 symbol_type: None,
2653 name: None,
2654 group_by: None,
2655 json: false,
2656 plain: false,
2657 csv: false,
2658 dot: false,
2659 markdown: false,
2660 tui: false,
2661 max_depth: 10,
2662 context: 0,
2663 limit: None,
2664 count: false,
2665 summary: false,
2666 xref: false,
2667 project: None,
2668 };
2669
2670 let ops = args.operations();
2671 assert_eq!(ops.len(), 4, "Expected 4 operations, got {:?}", ops);
2673
2674 let has_dead = ops.iter().any(|op| matches!(op, TraceOperation::DeadCode));
2676 let has_stats = ops.iter().any(|op| matches!(op, TraceOperation::Stats));
2677 let has_refs = ops
2678 .iter()
2679 .any(|op| matches!(op, TraceOperation::Refs { .. }));
2680 let has_trace = ops.iter().any(|op| matches!(op, TraceOperation::Trace(_)));
2681
2682 assert!(has_dead, "Missing DeadCode operation");
2683 assert!(has_stats, "Missing Stats operation");
2684 assert!(has_refs, "Missing Refs operation");
2685 assert!(has_trace, "Missing Trace operation");
2686 }
2687}
2688
2689#[allow(dead_code)]
2690pub fn debug_index_stats(project: &Project) -> Result<()> {
2691 let index = load_semantic_index(project)?;
2692
2693 println!("=== INDEX DEBUG ===");
2694 println!("Symbols: {}", index.symbols.len());
2695 println!("Tokens: {}", index.tokens.len());
2696 println!("symbol_by_name entries: {}", index.symbol_by_name.len());
2697 println!("token_by_name entries: {}", index.token_by_name.len());
2698
2699 println!("\nSample token names:");
2700 for (name, ids) in index.token_by_name.iter().take(10) {
2701 println!(" '{}' -> {} occurrences", name, ids.len());
2702 }
2703
2704 if let Some(ids) = index.tokens_by_name("userId") {
2705 println!("\n'userId' found: {} occurrences", ids.len());
2706 } else {
2707 println!("\n'userId' NOT FOUND in token_by_name");
2708 let matches = index.tokens_matching("userId");
2709 println!("Tokens containing 'userId': {}", matches.len());
2710 }
2711
2712 Ok(())
2713}