1use crate::cli::{Cli, Commands, ItemKindArg, OutputFormat, QueryCommands};
2use crate::graph::KnowledgeGraph;
3use crate::query::Query;
4use crate::visualization::{
5 DotGenerator, DotOptions, DotTheme, EdgeStyle, RankDir, SvgGenerator, SvgOptions,
6};
7use clap::CommandFactory;
8use clap_complete::generate;
9use std::fs;
10use std::io;
11
12#[must_use]
20#[allow(clippy::too_many_lines)]
21pub fn run_cli(cli: Cli) -> i32 {
22 match cli.command {
23 Commands::Completions { shell } => {
24 let mut cmd = crate::cli::Cli::command();
25 let bin_name = env!("CARGO_PKG_NAME");
26 let mut out = io::stdout();
27 generate(shell, &mut cmd, bin_name, &mut out);
28 0
29 }
30 Commands::Build {
31 path,
32 config,
33 no_ignore,
34 no_cache,
35 rebuild,
36 json,
37 dot,
38 svg,
39 dot_clusters,
40 dot_legend,
41 dot_theme,
42 dot_rankdir,
43 dot_splines,
44 dot_rounded,
45 svg_interactive,
46 save,
47 } => {
48 let mode = if rebuild {
50 crate::utils::cache::CacheMode::Rebuild
51 } else if no_cache {
52 crate::utils::cache::CacheMode::Ignore
53 } else {
54 crate::utils::cache::CacheMode::Use
55 };
56
57 let build_path = path.as_ref().unwrap().as_path();
58 if matches!(mode, crate::utils::cache::CacheMode::Rebuild) {
59 crate::utils::cache::clear_cache(build_path);
60 }
61
62 let graph = match KnowledgeGraph::build_from_directory_with_cache_opts(
63 build_path, mode, no_ignore,
64 ) {
65 Ok(g) => g,
66 Err(e) => {
67 eprintln!("Build failed: {e}");
68 return 1;
69 }
70 };
71
72 if let Some(json_path) = json {
74 let serialized =
75 serde_json::to_string_pretty(&graph).expect("serialize graph to JSON");
76 if let Err(e) = fs::write(&json_path, serialized) {
77 eprintln!("Failed to write JSON output {json_path}: {e}");
78 }
79 }
80
81 let mut clusters = matches!(dot_clusters, crate::cli::OnOffArg::On);
83 let mut legend = matches!(dot_legend, crate::cli::OnOffArg::On);
84 let mut theme = match dot_theme {
85 crate::cli::DotThemeArg::Dark => DotTheme::Dark,
86 crate::cli::DotThemeArg::Light => DotTheme::Light,
87 };
88 let mut rankdir = match dot_rankdir {
89 crate::cli::DotRankDirArg::TB => RankDir::TB,
90 crate::cli::DotRankDirArg::LR => RankDir::LR,
91 };
92 let mut splines = match dot_splines {
93 crate::cli::DotSplinesArg::Ortho => EdgeStyle::Ortho,
94 crate::cli::DotSplinesArg::Polyline => EdgeStyle::Polyline,
95 crate::cli::DotSplinesArg::Curved => EdgeStyle::Curved,
96 };
97 let mut rounded = matches!(dot_rounded, crate::cli::OnOffArg::On);
98 if let Some(cfg_path) = config.as_ref() {
99 if let Some(cfg) =
100 crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
101 {
102 if let Some(dot) = cfg.dot {
103 if let Some(v) = dot.clusters {
104 clusters = v;
105 }
106 if let Some(v) = dot.legend {
107 legend = v;
108 }
109 if let Some(v) = dot.theme {
110 theme = if v == "dark" { DotTheme::Dark } else { DotTheme::Light };
111 }
112 if let Some(v) = dot.rankdir {
113 rankdir = if v == "TB" { RankDir::TB } else { RankDir::LR };
114 }
115 if let Some(v) = dot.splines {
116 splines = match v.as_str() {
117 "ortho" => EdgeStyle::Ortho,
118 "polyline" => EdgeStyle::Polyline,
119 _ => EdgeStyle::Curved,
120 };
121 }
122 if let Some(v) = dot.rounded {
123 rounded = v;
124 }
125 }
126 }
127 }
128 let dot_opts = DotOptions { clusters, legend, theme, rankdir, splines, rounded };
129
130 if let Some(dot_path) = dot {
131 match DotGenerator::new().generate_dot_with_options(&graph, dot_opts) {
132 Ok(content) => {
133 if let Err(e) = fs::write(&dot_path, content) {
134 eprintln!("Failed to write DOT output {dot_path}: {e}");
135 }
136 }
137 Err(e) => eprintln!("Visualization error: {e}"),
138 }
139 }
140
141 if let Some(svg_path) = svg {
142 let mut interactive = matches!(svg_interactive, crate::cli::OnOffArg::On);
143 if let Some(cfg_path) = config.as_ref() {
144 if let Some(cfg) =
145 crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
146 {
147 if let Some(svg) = cfg.svg {
148 if let Some(v) = svg.interactive {
149 interactive = v;
150 }
151 }
152 }
153 }
154 let svg_opts = SvgOptions { dot: dot_opts, interactive };
155 match SvgGenerator::new().generate_svg_with_options(&graph, svg_opts) {
156 Ok(content) => {
157 if let Err(e) = fs::write(&svg_path, content) {
158 eprintln!("Failed to write SVG output {svg_path}: {e}");
159 }
160 }
161 Err(e) => eprintln!("Visualization error: {e}"),
162 }
163 }
164
165 if let Some(save_path) = save {
166 if let Err(e) = KnowledgeGraph::save_json(&graph, std::path::Path::new(&save_path))
167 {
168 eprintln!("Failed to save graph JSON {save_path}: {e}");
169 }
170 }
171
172 if !cli.quiet {
173 println!("Build completed for path: {}", build_path.display());
174 }
175 0
176 }
177 Commands::Query { query } => match query {
178 QueryCommands::ConnectedFiles {
179 path,
180 config,
181 no_ignore,
182 file,
183 graph: graph_path,
184 format,
185 offset,
186 limit,
187 file_pos: _,
188 } => {
189 let graph = if let Some(p) = graph_path {
190 match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
191 Ok(g) => g,
192 Err(e) => {
193 eprintln!("Load graph failed: {e}");
194 return 1;
195 }
196 }
197 } else {
198 let res = match KnowledgeGraph::build_from_directory_opts(
199 path.as_ref().unwrap().as_path(),
200 no_ignore,
201 ) {
202 Ok(g) => g,
203 Err(e) => {
204 eprintln!("Build failed: {e}");
205 return 1;
206 }
207 };
208 res
209 };
210 let file = match file.as_ref() {
211 Some(f) => f,
212 None => {
213 eprintln!("Missing file argument. Provide <file> or --file <path>.");
214 return 2;
215 }
216 };
217 let q = crate::query::ConnectedFilesQuery::new(file);
218 let results = q.run(&graph);
219 let fmt = if let Some(cfg_path) = config.as_ref() {
220 if let Some(cfg) =
221 crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
222 {
223 match cfg.query.and_then(|q| q.default_format).as_deref() {
224 Some("json") => OutputFormat::Json,
225 Some("text") => OutputFormat::Text,
226 _ => format,
227 }
228 } else {
229 format
230 }
231 } else {
232 format
233 };
234 let start = offset.min(results.len());
235 let end = match limit {
236 Some(l) => (start + l).min(results.len()),
237 None => results.len(),
238 };
239 let page = &results[start..end];
240 if matches!(fmt, OutputFormat::Json) {
241 let out: Vec<String> = page.iter().map(|p| p.display().to_string()).collect();
242 match serde_json::to_string_pretty(&out) {
243 Ok(s) => println!("{s}"),
244 Err(e) => {
245 eprintln!("JSON encode error: {e}");
246 return 1;
247 }
248 }
249 } else {
250 let rows: Vec<Vec<String>> = page
251 .iter()
252 .enumerate()
253 .map(|(i, p)| vec![format!("{}", start + i + 1), p.display().to_string()])
254 .collect();
255 let table = crate::utils::table::render(&["#", "Path"], &rows);
256 println!("{table}");
257 }
258 0
259 }
260 QueryCommands::FunctionUsage {
261 path,
262 config,
263 no_ignore,
264 function,
265 direction,
266 graph: graph_path,
267 format,
268 offset,
269 limit,
270 } => {
271 let graph = if let Some(p) = graph_path {
272 match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
273 Ok(g) => g,
274 Err(e) => {
275 eprintln!("Load graph failed: {e}");
276 return 1;
277 }
278 }
279 } else {
280 if no_ignore {
281 std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
282 }
283 let res = match KnowledgeGraph::build_from_directory(
284 path.as_ref().unwrap().as_path(),
285 ) {
286 Ok(g) => g,
287 Err(e) => {
288 eprintln!("Build failed: {e}");
289 if no_ignore {
290 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
291 }
292 return 1;
293 }
294 };
295 if no_ignore {
296 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
297 }
298 res
299 };
300 let dir = match direction {
301 crate::cli::Direction::Callees => crate::query::UsageDirection::Callees,
302 crate::cli::Direction::Callers => crate::query::UsageDirection::Callers,
303 };
304 let q = crate::query::FunctionUsageQuery { function, direction: dir };
305 let results = q.run(&graph);
306 let fmt = if let Some(cfg_path) = config.as_ref() {
307 if let Some(cfg) =
308 crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
309 {
310 match cfg.query.and_then(|q| q.default_format).as_deref() {
311 Some("json") => OutputFormat::Json,
312 Some("text") => OutputFormat::Text,
313 _ => format,
314 }
315 } else {
316 format
317 }
318 } else {
319 format
320 };
321 let start = offset.min(results.len());
322 let end = match limit {
323 Some(l) => (start + l).min(results.len()),
324 None => results.len(),
325 };
326 let page = &results[start..end];
327 if matches!(fmt, OutputFormat::Json) {
328 let out: Vec<String> = page.iter().map(|p| p.display().to_string()).collect();
329 match serde_json::to_string_pretty(&out) {
330 Ok(s) => println!("{s}"),
331 Err(e) => {
332 eprintln!("JSON encode error: {e}");
333 return 1;
334 }
335 }
336 } else {
337 for p in page {
338 println!("{}", p.display());
339 }
340 }
341 0
342 }
343 QueryCommands::Cycles {
344 path,
345 config,
346 no_ignore,
347 graph: graph_path,
348 format,
349 offset,
350 limit,
351 } => {
352 let graph = if let Some(p) = graph_path {
353 match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
354 Ok(g) => g,
355 Err(e) => {
356 eprintln!("Load graph failed: {e}");
357 return 1;
358 }
359 }
360 } else {
361 if no_ignore {
362 std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
363 }
364 let res = match KnowledgeGraph::build_from_directory(
365 path.as_ref().unwrap().as_path(),
366 ) {
367 Ok(g) => g,
368 Err(e) => {
369 eprintln!("Build failed: {e}");
370 if no_ignore {
371 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
372 }
373 return 1;
374 }
375 };
376 if no_ignore {
377 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
378 }
379 res
380 };
381 let q = crate::query::CycleDetectionQuery::new();
382 let cycles = q.run(&graph);
383 let fmt = if let Some(cfg_path) = config.as_ref() {
384 if let Some(cfg) =
385 crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
386 {
387 match cfg.query.and_then(|q| q.default_format).as_deref() {
388 Some("json") => OutputFormat::Json,
389 Some("text") => OutputFormat::Text,
390 _ => format,
391 }
392 } else {
393 format
394 }
395 } else {
396 format
397 };
398 let start = offset.min(cycles.len());
399 let end = match limit {
400 Some(l) => (start + l).min(cycles.len()),
401 None => cycles.len(),
402 };
403 let page = &cycles[start..end];
404 if matches!(fmt, OutputFormat::Json) {
405 let out: Vec<Vec<String>> = page
406 .iter()
407 .map(|cyc| cyc.iter().map(|p| p.display().to_string()).collect())
408 .collect();
409 match serde_json::to_string_pretty(&out) {
410 Ok(s) => println!("{s}"),
411 Err(e) => {
412 eprintln!("JSON encode error: {e}");
413 return 1;
414 }
415 }
416 } else {
417 for cyc in page {
418 let parts: Vec<String> =
419 cyc.iter().map(|p| p.display().to_string()).collect();
420 println!("{}", parts.join(" -> "));
421 }
422 }
423 0
424 }
425 QueryCommands::Path {
426 path,
427 config,
428 no_ignore,
429 from,
430 to,
431 graph: graph_path,
432 format,
433 offset,
434 limit,
435 } => {
436 let graph = if let Some(p) = graph_path {
437 match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
438 Ok(g) => g,
439 Err(e) => {
440 eprintln!("Load graph failed: {e}");
441 return 1;
442 }
443 }
444 } else {
445 if no_ignore {
446 std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
447 }
448 let res = match KnowledgeGraph::build_from_directory(
449 path.as_ref().unwrap().as_path(),
450 ) {
451 Ok(g) => g,
452 Err(e) => {
453 eprintln!("Build failed: {e}");
454 if no_ignore {
455 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
456 }
457 return 1;
458 }
459 };
460 if no_ignore {
461 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
462 }
463 res
464 };
465 let q = crate::query::ShortestPathQuery::new(&from, &to);
466 let results = q.run(&graph);
467 let fmt = if let Some(cfg_path) = config.as_ref() {
468 if let Some(cfg) =
469 crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
470 {
471 match cfg.query.and_then(|q| q.default_format).as_deref() {
472 Some("json") => OutputFormat::Json,
473 Some("text") => OutputFormat::Text,
474 _ => format,
475 }
476 } else {
477 format
478 }
479 } else {
480 format
481 };
482 if matches!(fmt, OutputFormat::Json) {
483 let start = offset.min(results.len());
484 let end = match limit {
485 Some(l) => (start + l).min(results.len()),
486 None => results.len(),
487 };
488 let page = &results[start..end];
489 let out: Vec<String> = page.iter().map(|p| p.display().to_string()).collect();
490 match serde_json::to_string_pretty(&out) {
491 Ok(s) => println!("{s}"),
492 Err(e) => {
493 eprintln!("JSON encode error: {e}");
494 return 1;
495 }
496 }
497 } else if results.is_empty() {
498 println!("<no path>");
499 } else {
500 let start = offset.min(results.len());
501 let end = match limit {
502 Some(l) => (start + l).min(results.len()),
503 None => results.len(),
504 };
505 let page = &results[start..end];
506 let rows: Vec<Vec<String>> = page
507 .iter()
508 .enumerate()
509 .map(|(i, p)| vec![format!("{}", start + i + 1), p.display().to_string()])
510 .collect();
511 let table = crate::utils::table::render(&["Step", "Path"], &rows);
512 println!("{table}");
513 }
514 0
515 }
516 QueryCommands::Hubs {
517 path,
518 config,
519 no_ignore,
520 graph: graph_path,
521 metric,
522 top,
523 format,
524 offset,
525 limit,
526 } => {
527 use crate::query::{CentralityMetric, HubsQuery};
528 let graph = if let Some(p) = graph_path {
529 match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
530 Ok(g) => g,
531 Err(e) => {
532 eprintln!("Load graph failed: {e}");
533 return 1;
534 }
535 }
536 } else {
537 if no_ignore {
538 std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
539 }
540 let res = match KnowledgeGraph::build_from_directory(
541 path.as_ref().unwrap().as_path(),
542 ) {
543 Ok(g) => g,
544 Err(e) => {
545 eprintln!("Build failed: {e}");
546 if no_ignore {
547 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
548 }
549 return 1;
550 }
551 };
552 if no_ignore {
553 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
554 }
555 res
556 };
557 let m = match metric {
558 crate::cli::CentralityMetricArg::In => CentralityMetric::In,
559 crate::cli::CentralityMetricArg::Out => CentralityMetric::Out,
560 crate::cli::CentralityMetricArg::Total => CentralityMetric::Total,
561 };
562 let q = HubsQuery::new(m, top);
563 let rows = q.run(&graph);
564 let fmt = if let Some(cfg_path) = config.as_ref() {
565 if let Some(cfg) =
566 crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
567 {
568 match cfg.query.and_then(|q| q.default_format).as_deref() {
569 Some("json") => OutputFormat::Json,
570 Some("text") => OutputFormat::Text,
571 _ => format,
572 }
573 } else {
574 format
575 }
576 } else {
577 format
578 };
579 let start = offset.min(rows.len());
580 let end = match limit {
581 Some(l) => (start + l).min(rows.len()),
582 None => rows.len(),
583 };
584 let page = &rows[start..end];
585 if matches!(fmt, OutputFormat::Json) {
586 #[derive(serde::Serialize)]
587 struct HubRow {
588 path: String,
589 indegree: usize,
590 outdegree: usize,
591 }
592 let out: Vec<HubRow> = page
593 .iter()
594 .map(|(p, i, o)| HubRow {
595 path: p.display().to_string(),
596 indegree: *i,
597 outdegree: *o,
598 })
599 .collect();
600 match serde_json::to_string_pretty(&out) {
601 Ok(s) => println!("{s}"),
602 Err(e) => {
603 eprintln!("JSON encode error: {e}");
604 return 1;
605 }
606 }
607 } else {
608 let body: Vec<Vec<String>> = if cli.verbose == 0 {
609 page.iter()
610 .map(|(p, i, o)| vec![p.display().to_string(), (i + o).to_string()])
611 .collect()
612 } else {
613 page.iter()
614 .map(|(p, i, o)| {
615 vec![
616 p.display().to_string(),
617 i.to_string(),
618 o.to_string(),
619 (i + o).to_string(),
620 ]
621 })
622 .collect()
623 };
624 let headers: &[&str] = if cli.verbose == 0 {
625 &["Path", "Total"]
626 } else {
627 &["Path", "In", "Out", "Total"]
628 };
629 let table = crate::utils::table::render(headers, &body);
630 println!("{table}");
631 }
632 0
633 }
634 QueryCommands::ModuleCentrality {
635 path,
636 config,
637 no_ignore,
638 graph: graph_path,
639 metric,
640 top,
641 format,
642 offset,
643 limit,
644 } => {
645 use crate::query::{CentralityMetric, ModuleCentralityQuery};
646 let graph = if let Some(p) = graph_path {
647 match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
648 Ok(g) => g,
649 Err(e) => {
650 eprintln!("Load graph failed: {e}");
651 return 1;
652 }
653 }
654 } else {
655 if no_ignore {
656 std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
657 }
658 let res = match KnowledgeGraph::build_from_directory(
659 path.as_ref().unwrap().as_path(),
660 ) {
661 Ok(g) => g,
662 Err(e) => {
663 eprintln!("Build failed: {e}");
664 if no_ignore {
665 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
666 }
667 return 1;
668 }
669 };
670 if no_ignore {
671 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
672 }
673 res
674 };
675 let m = match metric {
676 crate::cli::CentralityMetricArg::In => CentralityMetric::In,
677 crate::cli::CentralityMetricArg::Out => CentralityMetric::Out,
678 crate::cli::CentralityMetricArg::Total => CentralityMetric::Total,
679 };
680 let q = ModuleCentralityQuery::new(m, top);
681 let rows = q.run(&graph);
682 let fmt = if let Some(cfg_path) = config.as_ref() {
683 if let Some(cfg) =
684 crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
685 {
686 match cfg.query.and_then(|q| q.default_format).as_deref() {
687 Some("json") => OutputFormat::Json,
688 Some("text") => OutputFormat::Text,
689 _ => format,
690 }
691 } else {
692 format
693 }
694 } else {
695 format
696 };
697 let start = offset.min(rows.len());
698 let end = match limit {
699 Some(l) => (start + l).min(rows.len()),
700 None => rows.len(),
701 };
702 let page = &rows[start..end];
703 if matches!(fmt, OutputFormat::Json) {
704 #[derive(serde::Serialize)]
705 struct Row {
706 module: String,
707 indegree: usize,
708 outdegree: usize,
709 }
710 let out: Vec<Row> = page
711 .iter()
712 .map(|(p, i, o)| Row {
713 module: p.display().to_string(),
714 indegree: *i,
715 outdegree: *o,
716 })
717 .collect();
718 match serde_json::to_string_pretty(&out) {
719 Ok(s) => println!("{s}"),
720 Err(e) => {
721 eprintln!("JSON encode error: {e}");
722 return 1;
723 }
724 }
725 } else {
726 let body: Vec<Vec<String>> = if cli.verbose == 0 {
727 page.iter()
728 .map(|(p, i, o)| vec![p.display().to_string(), (i + o).to_string()])
729 .collect()
730 } else {
731 page.iter()
732 .map(|(p, i, o)| {
733 vec![
734 p.display().to_string(),
735 i.to_string(),
736 o.to_string(),
737 (i + o).to_string(),
738 ]
739 })
740 .collect()
741 };
742 let headers: &[&str] = if cli.verbose == 0 {
743 &["Module", "Total"]
744 } else {
745 &["Module", "In", "Out", "Total"]
746 };
747 let table = crate::utils::table::render(headers, &body);
748 println!("{table}");
749 }
750 0
751 }
752 QueryCommands::TraitImpls {
753 path,
754 config,
755 no_ignore,
756 r#trait,
757 graph: graph_path,
758 format,
759 offset,
760 limit,
761 } => {
762 use crate::query::TraitImplsQuery;
763 let graph = if let Some(p) = graph_path {
764 match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
765 Ok(g) => g,
766 Err(e) => {
767 eprintln!("Load graph failed: {e}");
768 return 1;
769 }
770 }
771 } else {
772 if no_ignore {
773 std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
774 }
775 let res = match KnowledgeGraph::build_from_directory(
776 path.as_ref().unwrap().as_path(),
777 ) {
778 Ok(g) => g,
779 Err(e) => {
780 eprintln!("Build failed: {e}");
781 if no_ignore {
782 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
783 }
784 return 1;
785 }
786 };
787 if no_ignore {
788 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
789 }
790 res
791 };
792 let q = TraitImplsQuery::new(&r#trait);
793 let rows = q.run(&graph);
794 let fmt = if let Some(cfg_path) = config.as_ref() {
795 if let Some(cfg) =
796 crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
797 {
798 match cfg.query.and_then(|q| q.default_format).as_deref() {
799 Some("json") => OutputFormat::Json,
800 Some("text") => OutputFormat::Text,
801 _ => format,
802 }
803 } else {
804 format
805 }
806 } else {
807 format
808 };
809 let start = offset.min(rows.len());
810 let end = match limit {
811 Some(l) => (start + l).min(rows.len()),
812 None => rows.len(),
813 };
814 let page = &rows[start..end];
815 if matches!(fmt, OutputFormat::Json) {
816 #[derive(serde::Serialize)]
817 struct Row {
818 path: String,
819 r#type: String,
820 }
821 let out: Vec<Row> = page
822 .iter()
823 .map(|(p, t)| Row { path: p.display().to_string(), r#type: t.to_string() })
824 .collect();
825 match serde_json::to_string_pretty(&out) {
826 Ok(s) => println!("{s}"),
827 Err(e) => {
828 eprintln!("JSON encode error: {e}");
829 return 1;
830 }
831 }
832 } else if rows.is_empty() {
833 println!("<no implementations found>");
834 } else {
835 let body: Vec<Vec<String>> = if cli.verbose == 0 {
836 page.iter().map(|(p, _t)| vec![p.display().to_string()]).collect()
837 } else {
838 page.iter().map(|(p, t)| vec![p.display().to_string(), t.clone()]).collect()
839 };
840 let headers: &[&str] =
841 if cli.verbose == 0 { &["Path"] } else { &["Path", "Type"] };
842 let table = crate::utils::table::render(headers, &body);
843 println!("{table}");
844 }
845 0
846 }
847 QueryCommands::UnreferencedItems {
848 path,
849 config,
850 no_ignore,
851 include_public,
852 exclude,
853 graph: graph_path,
854 format,
855 offset,
856 limit,
857 } => {
858 use crate::query::UnreferencedItemsQuery;
859 let graph = if let Some(p) = graph_path {
860 match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
861 Ok(g) => g,
862 Err(e) => {
863 eprintln!("Load graph failed: {e}");
864 return 1;
865 }
866 }
867 } else {
868 if no_ignore {
869 std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
870 }
871 let res = match KnowledgeGraph::build_from_directory(
872 path.as_ref().unwrap().as_path(),
873 ) {
874 Ok(g) => g,
875 Err(e) => {
876 eprintln!("Build failed: {e}");
877 if no_ignore {
878 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
879 }
880 return 1;
881 }
882 };
883 if no_ignore {
884 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
885 }
886 res
887 };
888 let exclude_re = if let Some(pat) = exclude.as_ref() {
889 match regex::Regex::new(pat) {
890 Ok(r) => Some(r),
891 Err(e) => {
892 eprintln!("Invalid --exclude regex: {e}");
893 return 1;
894 }
895 }
896 } else {
897 None
898 };
899 let q = UnreferencedItemsQuery::new(include_public, exclude_re);
900 let rows = q.run(&graph);
901 let fmt = if let Some(cfg_path) = config.as_ref() {
902 if let Some(cfg) =
903 crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
904 {
905 match cfg.query.and_then(|q| q.default_format).as_deref() {
906 Some("json") => OutputFormat::Json,
907 Some("text") => OutputFormat::Text,
908 _ => format,
909 }
910 } else {
911 format
912 }
913 } else {
914 format
915 };
916 let start = offset.min(rows.len());
917 let end = match limit {
918 Some(l) => (start + l).min(rows.len()),
919 None => rows.len(),
920 };
921 let page = &rows[start..end];
922 if matches!(fmt, OutputFormat::Json) {
923 #[derive(serde::Serialize)]
924 struct Row {
925 path: String,
926 id: String,
927 name: String,
928 kind: String,
929 visibility: String,
930 }
931 let out: Vec<Row> = page
932 .iter()
933 .map(|(p, id, name, kind, vis)| Row {
934 path: p.display().to_string(),
935 id: id.clone(),
936 name: name.clone(),
937 kind: kind.clone(),
938 visibility: vis.clone(),
939 })
940 .collect();
941 match serde_json::to_string_pretty(&out) {
942 Ok(s) => println!("{s}"),
943 Err(e) => {
944 eprintln!("JSON encode error: {e}");
945 return 1;
946 }
947 }
948 } else if rows.is_empty() {
949 println!("<no unreferenced items>");
950 } else {
951 let body: Vec<Vec<String>> = if cli.verbose == 0 {
952 page.iter()
953 .map(|(p, _id, name, _kind, _vis)| {
954 vec![p.display().to_string(), name.clone()]
955 })
956 .collect()
957 } else {
958 page.iter()
959 .map(|(p, id, name, kind, vis)| {
960 vec![
961 p.display().to_string(),
962 id.clone(),
963 name.clone(),
964 kind.clone(),
965 vis.clone(),
966 ]
967 })
968 .collect()
969 };
970 let headers: &[&str] = if cli.verbose == 0 {
971 &["Path", "Name"]
972 } else {
973 &["Path", "ItemId", "Name", "Kind", "Vis"]
974 };
975 let table = crate::utils::table::render(headers, &body);
976 println!("{table}");
977 }
978 0
979 }
980 QueryCommands::ItemInfo {
981 path,
982 config,
983 no_ignore,
984 item_id,
985 name,
986 kind,
987 graph: graph_path,
988 show_code,
989 format,
990 } => {
991 use crate::query::ItemInfoQuery;
992 let graph = if let Some(p) = graph_path {
993 match KnowledgeGraph::load_json(std::path::Path::new(&p)) {
994 Ok(g) => g,
995 Err(e) => {
996 eprintln!("Load graph failed: {e}");
997 return 1;
998 }
999 }
1000 } else {
1001 if no_ignore {
1002 std::env::set_var("KNOWLEDGE_RS_NO_IGNORE", "1");
1003 }
1004 let res = match KnowledgeGraph::build_from_directory(
1005 path.as_ref().unwrap().as_path(),
1006 ) {
1007 Ok(g) => g,
1008 Err(e) => {
1009 eprintln!("Build failed: {e}");
1010 if no_ignore {
1011 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
1012 }
1013 return 1;
1014 }
1015 };
1016 if no_ignore {
1017 std::env::remove_var("KNOWLEDGE_RS_NO_IGNORE");
1018 }
1019 res
1020 };
1021 let id = if let Some(raw_id) = item_id {
1023 crate::graph::ItemId(raw_id)
1024 } else if let Some(nm) = name {
1025 use crate::graph::{resolver::Resolver, ItemId, ItemType};
1026 use std::path::Path;
1027
1028 let resolver = Resolver::new(&graph);
1029 let mut ids: Vec<ItemId> = resolver.find_by_name(&nm);
1030 if let Some(k) = kind {
1031 ids.retain(|id| {
1032 matches!(
1033 (k, resolver.item_kind(id)),
1034 (ItemKindArg::Module, Some(ItemType::Module { .. }))
1035 | (ItemKindArg::Function, Some(ItemType::Function { .. }))
1036 | (ItemKindArg::Struct, Some(ItemType::Struct { .. }))
1037 | (ItemKindArg::Enum, Some(ItemType::Enum { .. }))
1038 | (ItemKindArg::Trait, Some(ItemType::Trait { .. }))
1039 | (ItemKindArg::Impl, Some(ItemType::Impl { .. }))
1040 | (ItemKindArg::Const, Some(ItemType::Const))
1041 | (ItemKindArg::Static, Some(ItemType::Static { .. }))
1042 | (ItemKindArg::Type, Some(ItemType::Type))
1043 | (ItemKindArg::Macro, Some(ItemType::Macro))
1044 )
1045 });
1046 }
1047 if ids.is_empty() {
1048 eprintln!("No item found with name '{nm}'.");
1049 if let Some(k) = kind {
1050 eprintln!("Hint: try a different --kind (current: {:?})", k);
1051 }
1052 return 1;
1053 }
1054
1055 let mut candidates: Vec<(ItemId, String, std::path::PathBuf)> =
1057 Vec::with_capacity(ids.len());
1058 for id in ids.into_iter() {
1059 let kind_s = match resolver.item_kind(&id) {
1060 Some(ItemType::Module { .. }) => "module",
1061 Some(ItemType::Function { .. }) => "function",
1062 Some(ItemType::Struct { .. }) => "struct",
1063 Some(ItemType::Enum { .. }) => "enum",
1064 Some(ItemType::Trait { .. }) => "trait",
1065 Some(ItemType::Impl { .. }) => "impl",
1066 Some(ItemType::Const) => "const",
1067 Some(ItemType::Static { .. }) => "static",
1068 Some(ItemType::Type) => "type",
1069 Some(ItemType::Macro) => "macro",
1070 None => "?",
1071 };
1072 if let Some(p) = resolver.item_path(&id) {
1073 candidates.push((id, kind_s.to_string(), p.clone()));
1074 }
1075 }
1076 if candidates.is_empty() {
1077 eprintln!("No item found with name '{nm}'.");
1078 return 1;
1079 }
1080
1081 let root_src: Option<std::path::PathBuf> = path
1083 .as_ref()
1084 .map(|pb| pb.join("src"))
1085 .or_else(|| std::env::current_dir().ok().map(|d| d.join("src")));
1086
1087 let mut ranked = candidates;
1089 if let Some(root_src_path) = root_src.as_ref() {
1090 let root_src_canon = root_src_path;
1091 ranked.sort_by(|a, b| {
1092 let a_in = a.2.starts_with(root_src_canon);
1093 let b_in = b.2.starts_with(root_src_canon);
1094 let in_cmp = b_in.cmp(&a_in); if in_cmp != std::cmp::Ordering::Equal {
1096 return in_cmp;
1097 }
1098 let depth = |p: &Path| -> usize {
1099 let comps: Vec<_> = p.components().collect();
1100 let mut seen_src = false;
1101 let mut c = 0usize;
1102 for comp in comps {
1103 if let std::path::Component::Normal(os) = comp {
1104 if os.to_str() == Some("src") {
1105 seen_src = true;
1106 continue;
1107 }
1108 if seen_src {
1109 c += 1;
1110 }
1111 }
1112 }
1113 c
1114 };
1115 let a_d = depth(&a.2);
1116 let b_d = depth(&b.2);
1117 let d_cmp = a_d.cmp(&b_d);
1118 if d_cmp != std::cmp::Ordering::Equal {
1119 return d_cmp;
1120 }
1121 a.2.cmp(&b.2)
1122 });
1123 } else {
1124 ranked.sort_by(|a, b| a.2.cmp(&b.2));
1125 }
1126
1127 let top = &ranked[0];
1129 let same_rank = ranked
1130 .iter()
1131 .take_while(|cand| {
1132 let a = cand;
1133 let b = top;
1134 let a_in = if let Some(r) = root_src.as_ref() {
1135 a.2.starts_with(r)
1136 } else {
1137 false
1138 };
1139 let b_in = if let Some(r) = root_src.as_ref() {
1140 b.2.starts_with(r)
1141 } else {
1142 false
1143 };
1144 let depth = |p: &Path| -> usize {
1145 let comps: Vec<_> = p.components().collect();
1146 let mut seen_src = false;
1147 let mut c = 0usize;
1148 for comp in comps {
1149 if let std::path::Component::Normal(os) = comp {
1150 if os.to_str() == Some("src") {
1151 seen_src = true;
1152 continue;
1153 }
1154 if seen_src {
1155 c += 1;
1156 }
1157 }
1158 }
1159 c
1160 };
1161 a_in == b_in && depth(&a.2) == depth(&b.2)
1162 })
1163 .count();
1164 if ranked.len() > 1 && same_rank > 1 {
1165 eprintln!("Ambiguous name '{nm}'. Top matches:");
1166 for (cid, ck, cp) in ranked.iter().take(10) {
1167 eprintln!("- id={} kind={} path={}", cid.0, ck, cp.display());
1168 }
1169 eprintln!("Disambiguate by providing --item-id or add --kind.");
1170 return 1;
1171 }
1172 top.0.clone()
1173 } else {
1174 eprintln!("Missing --item-id or --name for item-info.");
1175 return 1;
1176 };
1177 let q = ItemInfoQuery::new(id, show_code);
1178 let result = q.run(&graph);
1179 let fmt = if let Some(cfg_path) = config.as_ref() {
1180 if let Some(cfg) =
1181 crate::utils::config::load_config_at(std::path::Path::new(cfg_path))
1182 {
1183 match cfg.query.and_then(|q| q.default_format).as_deref() {
1184 Some("json") => OutputFormat::Json,
1185 Some("text") => OutputFormat::Text,
1186 _ => format,
1187 }
1188 } else {
1189 format
1190 }
1191 } else {
1192 format
1193 };
1194 if matches!(fmt, OutputFormat::Json) {
1195 let result = if cli.verbose == 0 {
1197 result.map(|mut info| {
1198 info.code = None;
1199 for r in &mut info.inbound {
1200 r.context.clear();
1201 }
1202 for r in &mut info.outbound {
1203 r.context.clear();
1204 }
1205 info
1206 })
1207 } else {
1208 result
1209 };
1210 match serde_json::to_string_pretty(&result) {
1211 Ok(s) => println!("{s}"),
1212 Err(e) => {
1213 eprintln!("JSON encode error: {e}");
1214 return 1;
1215 }
1216 }
1217 } else {
1218 match result {
1219 None => println!("<item not found>"),
1220 Some(info) => {
1221 println!("Item: {}", info.name);
1222 println!("Id: {}", info.id);
1223 println!("Kind: {}", info.kind);
1224 println!("Vis: {}", info.visibility);
1225 println!(
1226 "Location: {}:{}-{}",
1227 info.path, info.line_start, info.line_end
1228 );
1229 if cli.verbose == 0 {
1230 let callers: String = if info.inbound.is_empty() {
1231 "<none>".to_string()
1232 } else {
1233 info.inbound
1234 .iter()
1235 .map(|r| r.id.clone())
1236 .collect::<Vec<_>>()
1237 .join(", ")
1238 };
1239 let callees: String = if info.outbound.is_empty() {
1240 "<none>".to_string()
1241 } else {
1242 info.outbound
1243 .iter()
1244 .map(|r| r.id.clone())
1245 .collect::<Vec<_>>()
1246 .join(", ")
1247 };
1248 println!("\nCallers: {}", callers);
1249 println!("Callees: {}", callees);
1250 } else {
1251 if show_code {
1252 if let Some(code) = info.code.as_deref() {
1253 println!("\n--- code ---\n{}\n--- end code ---", code);
1254 }
1255 }
1256 if info.inbound.is_empty() {
1257 println!("\nCallers: <none>");
1258 } else {
1259 println!("\nCallers:");
1260 for r in info.inbound {
1261 println!(
1262 "- [{}] {} ({}) @ {} :: {}",
1263 r.relation, r.name, r.id, r.path, r.context
1264 );
1265 }
1266 }
1267 if info.outbound.is_empty() {
1268 println!("\nCallees: <none>");
1269 } else {
1270 println!("\nCallees:");
1271 for r in info.outbound {
1272 println!(
1273 "- [{}] {} ({}) @ {} :: {}",
1274 r.relation, r.name, r.id, r.path, r.context
1275 );
1276 }
1277 }
1278 }
1279 }
1280 }
1281 }
1282 0
1283 }
1284 },
1285 }
1286}