1use crate::Symbol;
6use crate::indexing::facade::IndexFacade;
7use crate::io::{
8 EntityType, ExitCode, OutputFormat, OutputManager, OutputStatus,
9 envelope::{EntityType as EnvelopeEntityType, Envelope, ResultCode},
10 schema::{OutputData, OutputMetadata, UnifiedOutput, UnifiedOutputBuilder},
11};
12use crate::symbol::context::SymbolContext;
13use serde::Serialize;
14use std::borrow::Cow;
15use std::fmt::Display;
16
17pub enum ResolveResult {
23 Found(Symbol),
25 NotFound,
27 Ambiguous(Vec<Symbol>),
29 InvalidId(String),
31}
32
33pub struct QueryContext<'a> {
41 indexer: &'a IndexFacade,
42 format: OutputFormat,
43 fields: Option<Vec<String>>,
44 entity_type: EnvelopeEntityType,
45 command_name: &'static str,
46}
47
48impl<'a> QueryContext<'a> {
49 pub fn new(
51 indexer: &'a IndexFacade,
52 format: OutputFormat,
53 fields: Option<Vec<String>>,
54 entity_type: EnvelopeEntityType,
55 command_name: &'static str,
56 ) -> Self {
57 Self {
58 indexer,
59 format,
60 fields,
61 entity_type,
62 command_name,
63 }
64 }
65
66 pub fn resolve_symbol(&self, query: &str, language: Option<&str>) -> ResolveResult {
68 if let Some(id_str) = query.strip_prefix("symbol_id:") {
70 match id_str.parse::<u32>() {
71 Ok(id) => match self.indexer.get_symbol(crate::SymbolId(id)) {
72 Some(sym) => ResolveResult::Found(sym),
73 None => ResolveResult::NotFound,
74 },
75 Err(_) => ResolveResult::InvalidId(id_str.to_string()),
76 }
77 } else {
78 let symbols = self.indexer.find_symbols_by_name(query, language);
80 match symbols.len() {
81 0 => ResolveResult::NotFound,
82 1 => ResolveResult::Found(symbols.into_iter().next().unwrap()),
83 _ => ResolveResult::Ambiguous(symbols),
84 }
85 }
86 }
87
88 pub fn handle_resolve_error(&self, result: ResolveResult, query: &str) -> ExitCode {
90 match result {
91 ResolveResult::Found(_) => ExitCode::Success, ResolveResult::NotFound => self.output_not_found(query),
93 ResolveResult::Ambiguous(symbols) => self.output_ambiguous(query, &symbols),
94 ResolveResult::InvalidId(id) => self.output_invalid_id(&id),
95 }
96 }
97
98 pub fn output_not_found(&self, query: &str) -> ExitCode {
100 if self.format == OutputFormat::Json {
101 let envelope: Envelope<()> = Envelope::not_found(format!(
102 "No symbol found for '{query}'"
103 ))
104 .with_entity_type(self.entity_type)
105 .with_query(query)
106 .with_hint(
107 "Use codanna retrieve symbol <name> to search, or try semantic_search_with_context"
108 .to_string(),
109 );
110
111 println!("{}", envelope.to_json().expect("envelope serialization"));
112 ExitCode::NotFound
113 } else {
114 eprintln!("Not found: '{query}'");
115 ExitCode::NotFound
116 }
117 }
118
119 pub fn output_ambiguous(&self, query: &str, symbols: &[Symbol]) -> ExitCode {
121 if self.format == OutputFormat::Json {
122 let suggestions: Vec<String> = symbols
124 .iter()
125 .take(10)
126 .map(|s| format!("symbol_id:{}", s.id.value()))
127 .collect();
128
129 let envelope: Envelope<()> = Envelope::error(
130 ResultCode::InvalidQuery,
131 format!(
132 "Ambiguous: found {} symbol(s) named '{query}'",
133 symbols.len()
134 ),
135 )
136 .with_entity_type(self.entity_type)
137 .with_query(query)
138 .with_hint(format!(
139 "Use: codanna retrieve {} symbol_id:<id>",
140 self.command_name
141 ));
142
143 let context: Vec<serde_json::Value> = symbols
145 .iter()
146 .take(10)
147 .map(|s| {
148 serde_json::json!({
149 "symbol_id": s.id.value(),
150 "kind": format!("{:?}", s.kind),
151 "file_path": s.file_path,
152 "line": s.range.start_line + 1
153 })
154 })
155 .collect();
156
157 let envelope = envelope.with_error_details(crate::io::envelope::ErrorDetails {
158 suggestions,
159 context: Some(serde_json::json!(context)),
160 });
161
162 println!("{}", envelope.to_json().expect("envelope serialization"));
163 ExitCode::GeneralError
164 } else {
165 eprintln!(
167 "Ambiguous: found {} symbol(s) named '{}':",
168 symbols.len(),
169 query
170 );
171 for (i, sym) in symbols.iter().take(10).enumerate() {
172 eprintln!(
173 " {}. symbol_id:{} - {:?} at {}:{}",
174 i + 1,
175 sym.id.value(),
176 sym.kind,
177 sym.file_path,
178 sym.range.start_line + 1
179 );
180 }
181 if symbols.len() > 10 {
182 eprintln!(" ... and {} more", symbols.len() - 10);
183 }
184 eprintln!(
185 "\nUse: codanna retrieve {} symbol_id:<id>",
186 self.command_name
187 );
188 ExitCode::GeneralError
189 }
190 }
191
192 pub fn output_invalid_id(&self, id: &str) -> ExitCode {
194 if self.format == OutputFormat::Json {
195 let envelope: Envelope<()> = Envelope::error(
196 ResultCode::InvalidQuery,
197 format!("Invalid symbol_id format: '{id}'"),
198 )
199 .with_hint("symbol_id must be a positive integer");
200
201 println!("{}", envelope.to_json().expect("envelope serialization"));
202 } else {
203 eprintln!("Invalid symbol_id format: {id}");
204 }
205 ExitCode::GeneralError
206 }
207
208 pub fn output_success<T: Serialize + Display>(
210 &self,
211 data: Vec<T>,
212 query: &str,
213 hint: Option<&str>,
214 ) -> ExitCode {
215 let count = data.len();
216
217 if self.format == OutputFormat::Json {
218 let mut envelope = Envelope::success(data)
219 .with_entity_type(self.entity_type)
220 .with_count(count)
221 .with_query(query)
222 .with_message(format!("Found {count} result(s)"));
223
224 if let Some(h) = hint {
225 envelope = envelope.with_hint(h);
226 }
227
228 let json = if let Some(ref fields) = self.fields {
229 envelope.to_json_with_fields(fields)
230 } else {
231 envelope.to_json()
232 };
233
234 println!("{}", json.expect("envelope serialization"));
235 ExitCode::Success
236 } else {
237 for item in &data {
239 println!("{item}");
240 }
241 ExitCode::Success
242 }
243 }
244
245 pub fn output_empty(&self, query: &str, message: &str) -> ExitCode {
247 if self.format == OutputFormat::Json {
248 let envelope: Envelope<Vec<()>> = Envelope::success(vec![])
249 .with_entity_type(self.entity_type)
250 .with_count(0)
251 .with_query(query)
252 .with_message(message);
253
254 println!("{}", envelope.to_json().expect("envelope serialization"));
255 } else {
256 println!("{message}");
257 }
258 ExitCode::Success
259 }
260}
261
262pub fn retrieve_symbol(
266 indexer: &IndexFacade,
267 name: &str,
268 language: Option<&str>,
269 format: OutputFormat,
270 fields: Option<Vec<String>>,
271) -> ExitCode {
272 use crate::symbol::context::ContextIncludes;
273
274 let symbols = if let Some(id_str) = name.strip_prefix("symbol_id:") {
276 match id_str.parse::<u32>() {
278 Ok(id) => match indexer.get_symbol(crate::SymbolId(id)) {
279 Some(sym) => vec![sym],
280 None => vec![],
281 },
282 Err(_) => {
283 if format == OutputFormat::Json {
285 let envelope: Envelope<()> = Envelope::error(
286 ResultCode::InvalidQuery,
287 format!("Invalid symbol_id format: '{id_str}'"),
288 )
289 .with_hint("symbol_id must be a positive integer");
290 println!("{}", envelope.to_json().expect("envelope serialization"));
291 } else {
292 eprintln!("Invalid symbol_id format: {id_str}");
293 }
294 return ExitCode::GeneralError;
295 }
296 }
297 } else {
298 indexer.find_symbols_by_name(name, language)
300 };
301
302 if symbols.is_empty() {
303 if format == OutputFormat::Json {
305 let envelope: Envelope<()> = Envelope::not_found(format!("No symbol found for '{name}'"))
306 .with_entity_type(EnvelopeEntityType::Symbol)
307 .with_query(name)
308 .with_hint("Use codanna retrieve search <query> for fuzzy matching, or try semantic_search_with_context");
309 println!("{}", envelope.to_json().expect("envelope serialization"));
310 } else {
311 eprintln!("Not found: '{name}'");
312 }
313 return ExitCode::NotFound;
314 }
315
316 let symbols_with_context: Vec<SymbolContext> = symbols
318 .into_iter()
319 .filter_map(|symbol| {
320 indexer.get_symbol_context(
321 symbol.id,
322 ContextIncludes::IMPLEMENTATIONS
323 | ContextIncludes::DEFINITIONS
324 | ContextIncludes::CALLERS,
325 )
326 })
327 .collect();
328
329 let count = symbols_with_context.len();
330
331 if format == OutputFormat::Json {
332 let mut envelope = Envelope::success(symbols_with_context)
333 .with_entity_type(EnvelopeEntityType::Symbol)
334 .with_count(count)
335 .with_query(name)
336 .with_message(format!("Found {count} symbol(s)"))
337 .with_hint("Use symbol_id for precise lookup in subsequent queries");
338
339 if let Some(lang) = language {
341 envelope = envelope.with_lang(lang);
342 }
343
344 let json = if let Some(ref f) = fields {
345 envelope.to_json_with_fields(f)
346 } else {
347 envelope.to_json()
348 };
349
350 println!("{}", json.expect("envelope serialization"));
351 ExitCode::Success
352 } else {
353 for ctx in &symbols_with_context {
355 println!("{ctx}");
356 }
357 ExitCode::Success
358 }
359}
360
361pub fn retrieve_callers(
365 indexer: &IndexFacade,
366 function: &str,
367 language: Option<&str>,
368 format: OutputFormat,
369 fields: Option<Vec<String>>,
370) -> ExitCode {
371 use crate::symbol::context::ContextIncludes;
372
373 let ctx = QueryContext::new(
375 indexer,
376 format,
377 fields.clone(),
378 EnvelopeEntityType::Callers,
379 "callers",
380 );
381
382 let symbol = match ctx.resolve_symbol(function, language) {
384 ResolveResult::Found(s) => s,
385 other => return ctx.handle_resolve_error(other, function),
386 };
387
388 let callers = indexer.get_calling_functions_with_metadata(symbol.id);
390
391 if callers.is_empty() {
393 return ctx.output_empty(function, &format!("No functions call '{function}'"));
394 }
395
396 let callers_with_context: Vec<SymbolContext> = callers
398 .into_iter()
399 .filter_map(|(caller, _metadata)| {
400 indexer.get_symbol_context(
401 caller.id,
402 ContextIncludes::CALLS | ContextIncludes::DEFINITIONS,
403 )
404 })
405 .collect();
406
407 let count = callers_with_context.len();
408
409 if format == OutputFormat::Json {
410 let mut envelope = Envelope::success(callers_with_context)
411 .with_entity_type(EnvelopeEntityType::Callers)
412 .with_count(count)
413 .with_query(function)
414 .with_message(format!("Found {count} caller(s)"))
415 .with_hint("Use symbol_id for precise lookup");
416
417 if let Some(lang) = language {
418 envelope = envelope.with_lang(lang);
419 }
420
421 let json = if let Some(ref f) = fields {
422 envelope.to_json_with_fields(f)
423 } else {
424 envelope.to_json()
425 };
426
427 println!("{}", json.expect("envelope serialization"));
428 ExitCode::Success
429 } else {
430 for ctx in &callers_with_context {
432 println!("{ctx}");
433 }
434 ExitCode::Success
435 }
436}
437
438pub fn retrieve_calls(
442 indexer: &IndexFacade,
443 function: &str,
444 language: Option<&str>,
445 format: OutputFormat,
446 fields: Option<Vec<String>>,
447) -> ExitCode {
448 use crate::symbol::context::ContextIncludes;
449
450 let ctx = QueryContext::new(
452 indexer,
453 format,
454 fields.clone(),
455 EnvelopeEntityType::Calls,
456 "calls",
457 );
458
459 let symbol = match ctx.resolve_symbol(function, language) {
461 ResolveResult::Found(s) => s,
462 other => return ctx.handle_resolve_error(other, function),
463 };
464
465 let calls = indexer.get_called_functions_with_metadata(symbol.id);
467
468 if calls.is_empty() {
470 return ctx.output_empty(function, &format!("'{function}' makes no function calls"));
471 }
472
473 let calls_with_context: Vec<SymbolContext> = calls
475 .into_iter()
476 .filter_map(|(called, _metadata)| {
477 indexer.get_symbol_context(
478 called.id,
479 ContextIncludes::CALLERS | ContextIncludes::DEFINITIONS,
480 )
481 })
482 .collect();
483
484 let count = calls_with_context.len();
485
486 if format == OutputFormat::Json {
487 let mut envelope = Envelope::success(calls_with_context)
488 .with_entity_type(EnvelopeEntityType::Calls)
489 .with_count(count)
490 .with_query(function)
491 .with_message(format!("Found {count} call(s)"))
492 .with_hint("Use symbol_id for precise lookup");
493
494 if let Some(lang) = language {
495 envelope = envelope.with_lang(lang);
496 }
497
498 let json = if let Some(ref f) = fields {
499 envelope.to_json_with_fields(f)
500 } else {
501 envelope.to_json()
502 };
503
504 println!("{}", json.expect("envelope serialization"));
505 ExitCode::Success
506 } else {
507 for ctx in &calls_with_context {
509 println!("{ctx}");
510 }
511 ExitCode::Success
512 }
513}
514
515pub fn retrieve_implementations(
519 indexer: &IndexFacade,
520 trait_name: &str,
521 language: Option<&str>,
522 format: OutputFormat,
523 fields: Option<Vec<String>>,
524) -> ExitCode {
525 use crate::symbol::context::ContextIncludes;
526
527 let ctx = QueryContext::new(
529 indexer,
530 format,
531 fields.clone(),
532 EnvelopeEntityType::Symbol, "implementations",
534 );
535
536 let trait_symbol = match ctx.resolve_symbol(trait_name, language) {
538 ResolveResult::Found(s) => s,
539 other => return ctx.handle_resolve_error(other, trait_name),
540 };
541
542 let implementations = indexer.get_implementations(trait_symbol.id);
544
545 if implementations.is_empty() {
547 return ctx.output_empty(
548 trait_name,
549 &format!("No implementations found for '{trait_name}'"),
550 );
551 }
552
553 let impls_with_context: Vec<SymbolContext> = implementations
555 .into_iter()
556 .filter_map(|symbol| {
557 indexer.get_symbol_context(
558 symbol.id,
559 ContextIncludes::DEFINITIONS | ContextIncludes::CALLERS,
560 )
561 })
562 .collect();
563
564 let count = impls_with_context.len();
565
566 if format == OutputFormat::Json {
567 let mut envelope = Envelope::success(impls_with_context)
568 .with_entity_type(EnvelopeEntityType::Symbol)
569 .with_count(count)
570 .with_query(trait_name)
571 .with_message(format!("Found {count} implementation(s)"))
572 .with_hint("Use symbol_id for precise lookup");
573
574 if let Some(lang) = language {
575 envelope = envelope.with_lang(lang);
576 }
577
578 let json = if let Some(ref f) = fields {
579 envelope.to_json_with_fields(f)
580 } else {
581 envelope.to_json()
582 };
583
584 println!("{}", json.expect("envelope serialization"));
585 ExitCode::Success
586 } else {
587 for ctx in &impls_with_context {
589 println!("{ctx}");
590 }
591 ExitCode::Success
592 }
593}
594
595pub fn retrieve_search(
599 indexer: &IndexFacade,
600 query: &str,
601 limit: usize,
602 kind: Option<&str>,
603 module: Option<&str>,
604 language: Option<&str>,
605 format: OutputFormat,
606 fields: Option<Vec<String>>,
607) -> ExitCode {
608 use crate::symbol::context::ContextIncludes;
609
610 let kind_filter = kind.and_then(|k| match k.to_lowercase().as_str() {
612 "function" => Some(crate::SymbolKind::Function),
613 "struct" => Some(crate::SymbolKind::Struct),
614 "trait" => Some(crate::SymbolKind::Trait),
615 "interface" => Some(crate::SymbolKind::Interface),
616 "class" => Some(crate::SymbolKind::Class),
617 "method" => Some(crate::SymbolKind::Method),
618 "field" => Some(crate::SymbolKind::Field),
619 "variable" => Some(crate::SymbolKind::Variable),
620 "constant" => Some(crate::SymbolKind::Constant),
621 "module" => Some(crate::SymbolKind::Module),
622 "typealias" => Some(crate::SymbolKind::TypeAlias),
623 "enum" => Some(crate::SymbolKind::Enum),
624 _ => {
625 eprintln!("Warning: Unknown symbol kind '{k}', ignoring filter");
626 None
627 }
628 });
629
630 let search_results = indexer
631 .search(query, limit, kind_filter, module, language)
632 .unwrap_or_default();
633
634 let results_with_context: Vec<SymbolContext> = search_results
636 .into_iter()
637 .filter_map(|result| {
638 indexer.get_symbol_context(
639 result.symbol_id,
640 ContextIncludes::IMPLEMENTATIONS
641 | ContextIncludes::DEFINITIONS
642 | ContextIncludes::CALLERS,
643 )
644 })
645 .collect();
646
647 let count = results_with_context.len();
648
649 if format == OutputFormat::Json {
650 let envelope = if results_with_context.is_empty() {
652 Envelope::not_found(format!("No results for '{query}'"))
653 .with_entity_type(EnvelopeEntityType::SearchResult)
654 .with_query(query)
655 .with_hint("Try broader search terms or use semantic_search_with_context")
656 } else {
657 let mut env = Envelope::success(results_with_context)
658 .with_entity_type(EnvelopeEntityType::SearchResult)
659 .with_count(count)
660 .with_query(query)
661 .with_message(format!("Found {count} result(s)"))
662 .with_hint("Use symbol_id for precise lookup");
663
664 if let Some(lang) = language {
665 env = env.with_lang(lang);
666 }
667 env
668 };
669
670 let json = if let Some(ref f) = fields {
671 envelope.to_json_with_fields(f)
672 } else {
673 envelope.to_json()
674 };
675
676 println!("{}", json.expect("envelope serialization"));
677
678 if count == 0 {
679 ExitCode::NotFound
680 } else {
681 ExitCode::Success
682 }
683 } else {
684 if results_with_context.is_empty() {
686 eprintln!("No results for '{query}'");
687 ExitCode::NotFound
688 } else {
689 for ctx in &results_with_context {
690 println!("{ctx}");
691 }
692 ExitCode::Success
693 }
694 }
695}
696
697#[allow(dead_code)]
705pub fn retrieve_impact(
706 indexer: &IndexFacade,
707 symbol_name: &str,
708 max_depth: usize,
709 format: OutputFormat,
710) -> ExitCode {
711 let mut output = OutputManager::new(format);
712 let symbols = indexer.find_symbols_by_name(symbol_name, None);
713
714 if symbols.is_empty() {
715 let unified = UnifiedOutput {
716 status: OutputStatus::NotFound,
717 entity_type: EntityType::Impact,
718 count: 0,
719 data: OutputData::<SymbolContext>::Empty,
720 metadata: Some(OutputMetadata {
721 query: Some(Cow::Borrowed(symbol_name)),
722 tool: None,
723 timing_ms: None,
724 truncated: None,
725 extra: Default::default(),
726 }),
727 guidance: None,
728 exit_code: ExitCode::NotFound,
729 };
730
731 match output.unified(unified) {
732 Ok(code) => code,
733 Err(e) => {
734 eprintln!("Error writing output: {e}");
735 ExitCode::GeneralError
736 }
737 }
738 } else {
739 let symbol = &symbols[0];
741 let impact_symbol_ids = indexer.get_impact_radius(symbol.id, Some(max_depth));
742
743 use crate::symbol::context::ContextIncludes;
745
746 let impact_with_path: Vec<SymbolContext> = impact_symbol_ids
747 .into_iter()
748 .filter_map(|symbol_id| {
749 indexer.get_symbol_context(
751 symbol_id,
752 ContextIncludes::CALLERS | ContextIncludes::CALLS,
753 )
754 })
755 .collect();
756
757 let unified = UnifiedOutputBuilder::items(impact_with_path, EntityType::Impact)
758 .with_metadata(OutputMetadata {
759 query: Some(Cow::Borrowed(symbol_name)),
760 tool: None,
761 timing_ms: None,
762 truncated: None,
763 extra: Default::default(),
764 })
765 .build();
766
767 match output.unified(unified) {
768 Ok(code) => code,
769 Err(e) => {
770 eprintln!("Error writing output: {e}");
771 ExitCode::GeneralError
772 }
773 }
774 }
775}
776
777pub fn retrieve_describe(
782 indexer: &IndexFacade,
783 symbol_name: &str,
784 language: Option<&str>,
785 format: OutputFormat,
786 fields: Option<Vec<String>>,
787) -> ExitCode {
788 let ctx = QueryContext::new(
790 indexer,
791 format,
792 fields.clone(),
793 EnvelopeEntityType::Symbol,
794 "describe",
795 );
796
797 let symbol = match ctx.resolve_symbol(symbol_name, language) {
799 ResolveResult::Found(s) => s,
800 other => return ctx.handle_resolve_error(other, symbol_name),
801 };
802
803 let file_path = SymbolContext::symbol_location(&symbol);
805
806 let mut context = SymbolContext {
807 symbol: symbol.clone(),
808 file_path,
809 relationships: Default::default(),
810 };
811
812 let calls = indexer.get_called_functions_with_metadata(symbol.id);
814 if !calls.is_empty() {
815 context.relationships.calls = Some(calls);
816 }
817
818 let callers = indexer.get_calling_functions_with_metadata(symbol.id);
820 if !callers.is_empty() {
821 context.relationships.called_by = Some(callers);
822 }
823
824 let deps = indexer.get_dependencies(symbol.id);
826 if let Some(defines) = deps.get(&crate::RelationKind::Defines) {
827 context.relationships.defines = Some(defines.clone());
828 }
829
830 use crate::SymbolKind;
832 match symbol.kind {
833 SymbolKind::Trait | SymbolKind::Interface => {
834 let implementations = indexer.get_implementations(symbol.id);
835 if !implementations.is_empty() {
836 context.relationships.implemented_by = Some(implementations);
837 }
838 }
839 SymbolKind::Struct | SymbolKind::Enum | SymbolKind::Class => {
840 let impls = indexer.get_implemented_traits(symbol.id);
842 if !impls.is_empty() {
843 context.relationships.implements = Some(impls);
844 }
845 }
846 _ => {}
847 }
848
849 match symbol.kind {
851 SymbolKind::Class | SymbolKind::Struct => {
852 let extends = indexer.get_extends(symbol.id);
854 if !extends.is_empty() {
855 context.relationships.extends = Some(extends);
856 }
857
858 let extended_by = indexer.get_extended_by(symbol.id);
860 if !extended_by.is_empty() {
861 context.relationships.extended_by = Some(extended_by);
862 }
863 }
864 _ => {}
865 }
866
867 let uses = indexer.get_uses(symbol.id);
869 if !uses.is_empty() {
870 context.relationships.uses = Some(uses);
871 }
872
873 let used_by = indexer.get_used_by(symbol.id);
874 if !used_by.is_empty() {
875 context.relationships.used_by = Some(used_by);
876 }
877
878 if format == OutputFormat::Json {
880 let mut envelope = Envelope::success(context)
881 .with_entity_type(EnvelopeEntityType::Symbol)
882 .with_count(1)
883 .with_query(symbol_name)
884 .with_message(format!("Symbol '{}' described", symbol.name))
885 .with_hint("Use callers/calls commands to explore relationships further");
886
887 if let Some(lang) = language {
888 envelope = envelope.with_lang(lang);
889 }
890
891 let json = if let Some(ref f) = fields {
892 envelope.to_json_with_fields(f)
893 } else {
894 envelope.to_json()
895 };
896
897 println!("{}", json.expect("envelope serialization"));
898 ExitCode::Success
899 } else {
900 println!("{context}");
902 ExitCode::Success
903 }
904}