Files
Kigi-CLI/crates/codegen/kigi-codebase-graph/src/bin/code_graph.rs
T
ZacharyZhang-NY a02b555e66 docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
2026-07-23 16:55:39 -04:00

387 lines
11 KiB
Rust

//! CLI tool for code graph navigation.
//!
//! Provides go-to-definition and go-to-references functionality.
//!
//! # Usage
//!
//! ```bash
//! # Build the index for a repository
//! code-graph index /path/to/repo
//!
//! # Build the index with custom cache location
//! code-graph index /path/to/repo --cache /path/to/cache.bin
//!
//! # Go to definition (by position)
//! code-graph definition /path/to/repo --file src/main.rs --row 10 --col 15
//!
//! # Go to definition (by symbol name)
//! code-graph definition /path/to/repo --symbol MyStruct
//!
//! # Go to references (by position)
//! code-graph references /path/to/repo --file src/main.rs --row 10 --col 15
//!
//! # Go to references (by symbol name)
//! code-graph references /path/to/repo --symbol MyStruct
//!
//! # Show index statistics
//! code-graph stats /path/to/repo
//! ```
use std::path::{Path, PathBuf};
use std::time::Instant;
use clap::{Parser, Subcommand};
use kigi_codebase_graph::{
IndexBuilder, Navigator, ScopeGraphIndex, get_cache_path, load_index, save_index,
};
// Use mimalloc for faster allocation
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[derive(Parser)]
#[command(name = "code-graph")]
#[command(author, version, about = "High-performance code navigation tool", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Build or rebuild the index for a repository
Index {
/// Path to the repository
path: PathBuf,
/// Custom cache file path (default: <repo>/.goto_index.bin)
#[arg(short, long)]
cache: Option<PathBuf>,
/// Force rebuild even if cache exists
#[arg(short, long)]
force: bool,
/// Number of threads to use
#[arg(short, long)]
threads: Option<usize>,
},
/// Go to definition for a symbol
Definition {
/// Path to the repository
path: PathBuf,
/// Custom cache file path (default: <repo>/.goto_index.bin)
#[arg(long)]
cache: Option<PathBuf>,
/// File path (for position-based lookup)
#[arg(short, long)]
file: Option<PathBuf>,
/// Row number (1-indexed, for position-based lookup)
#[arg(short, long)]
row: Option<usize>,
/// Column number (1-indexed, for position-based lookup)
#[arg(short, long)]
col: Option<usize>,
/// Symbol name (for direct lookup)
#[arg(short, long)]
symbol: Option<String>,
/// Output as JSON
#[arg(long)]
json: bool,
},
/// Go to references for a symbol
References {
/// Path to the repository
path: PathBuf,
/// Custom cache file path (default: <repo>/.goto_index.bin)
#[arg(long)]
cache: Option<PathBuf>,
/// File path (for position-based lookup)
#[arg(short, long)]
file: Option<PathBuf>,
/// Row number (1-indexed, for position-based lookup)
#[arg(short, long)]
row: Option<usize>,
/// Column number (1-indexed, for position-based lookup)
#[arg(short, long)]
col: Option<usize>,
/// Symbol name (for direct lookup)
#[arg(short, long)]
symbol: Option<String>,
/// Include definition in results
#[arg(long)]
include_definition: bool,
/// Output as JSON
#[arg(long)]
json: bool,
},
/// Show index statistics
Stats {
/// Path to the repository
path: PathBuf,
/// Custom cache file path (default: <repo>/.goto_index.bin)
#[arg(long)]
cache: Option<PathBuf>,
},
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Index {
path,
cache,
force,
threads,
} => {
cmd_index(&path, cache.as_deref(), force, threads);
}
Commands::Definition {
path,
cache,
file,
row,
col,
symbol,
json,
} => {
cmd_definition(&path, cache.as_deref(), file, row, col, symbol, json);
}
Commands::References {
path,
cache,
file,
row,
col,
symbol,
include_definition,
json,
} => {
cmd_references(
&path,
cache.as_deref(),
file,
row,
col,
symbol,
include_definition,
json,
);
}
Commands::Stats { path, cache } => {
cmd_stats(&path, cache.as_deref());
}
}
}
fn effective_cache_path(repo_path: &Path, custom_cache: Option<&Path>) -> PathBuf {
custom_cache
.map(|p| p.to_path_buf())
.unwrap_or_else(|| get_cache_path(repo_path))
}
fn load_or_build_index(repo_path: &Path, cache_path: &Path) -> ScopeGraphIndex {
if let Ok(index) = load_index(cache_path) {
println!("Loaded index from cache: {}", cache_path.display());
return index;
}
println!("Building index for: {}", repo_path.display());
let start = Instant::now();
let index = IndexBuilder::new()
.build(repo_path)
.expect("Failed to build index");
let elapsed = start.elapsed();
let (files, defs, refs) = index.stats();
println!(
"Built index: {} files, {} defs, {} refs in {:?}",
files, defs, refs, elapsed
);
if let Err(e) = save_index(cache_path, &index) {
println!("Warning: Failed to save cache: {}", e);
} else {
println!("Saved cache to: {}", cache_path.display());
}
index
}
fn cmd_index(path: &Path, custom_cache: Option<&Path>, _force: bool, threads: Option<usize>) {
let cache_path = effective_cache_path(path, custom_cache);
println!("Building index for: {}", path.display());
let start = Instant::now();
let mut builder = IndexBuilder::new();
if let Some(t) = threads {
builder = builder.with_threads(t);
}
let index = builder.build(path).expect("Failed to build index");
let elapsed = start.elapsed();
let (files, defs, refs) = index.stats();
println!("Index built successfully!");
println!(" Files indexed: {}", files);
println!(" Definitions: {}", defs);
println!(" References: {}", refs);
println!(" Aliases: {}", index.alias_count());
println!(" Time: {:?}", elapsed);
// Always save when explicitly indexing
if let Err(e) = save_index(&cache_path, &index) {
println!("Error saving cache: {}", e);
std::process::exit(1);
} else {
println!(" Cache saved: {}", cache_path.display());
}
}
fn cmd_definition(
repo_path: &Path,
custom_cache: Option<&Path>,
file: Option<PathBuf>,
row: Option<usize>,
col: Option<usize>,
symbol: Option<String>,
json: bool,
) {
let cache_path = effective_cache_path(repo_path, custom_cache);
let index = load_or_build_index(repo_path, &cache_path);
let navigator = Navigator::new(index);
let result = match (file, row, col, symbol) {
(Some(file_path), Some(r), Some(c), _) => {
let abs_path = if file_path.is_absolute() {
file_path
} else {
repo_path.join(&file_path)
};
match navigator.goto_definition(&abs_path, r, c) {
Ok(r) => r,
Err(e) => {
println!("Error: {}", e);
std::process::exit(1);
}
}
}
(_, _, _, Some(sym)) => navigator.goto_definition_by_name(&sym, None),
_ => {
println!("Error: Must provide either --file, --row, --col OR --symbol");
std::process::exit(1);
}
};
if json {
print_json(&result);
} else {
println!("Symbol: {}", result.symbol);
println!("Definitions ({}):", result.locations.len());
for loc in &result.locations {
println!(" {}:{}", loc.path, loc.line);
}
}
}
fn cmd_references(
repo_path: &Path,
custom_cache: Option<&Path>,
file: Option<PathBuf>,
row: Option<usize>,
col: Option<usize>,
symbol: Option<String>,
include_definition: bool,
json: bool,
) {
let cache_path = effective_cache_path(repo_path, custom_cache);
let index = load_or_build_index(repo_path, &cache_path);
let navigator = Navigator::new(index);
let result = match (file, row, col, symbol) {
(Some(file_path), Some(r), Some(c), _) => {
let abs_path = if file_path.is_absolute() {
file_path
} else {
repo_path.join(&file_path)
};
match navigator.goto_references(&abs_path, r, c, include_definition) {
Ok(r) => r,
Err(e) => {
println!("Error: {}", e);
std::process::exit(1);
}
}
}
(_, _, _, Some(sym)) => navigator.goto_references_by_name(&sym, None, include_definition),
_ => {
println!("Error: Must provide either --file, --row, --col OR --symbol");
std::process::exit(1);
}
};
if json {
print_json(&result);
} else {
println!("Symbol: {}", result.symbol);
println!("References ({}):", result.locations.len());
for loc in &result.locations {
if let Some(sym) = &loc.symbol {
println!(" {}:{} (as {})", loc.path, loc.line, sym);
} else {
println!(" {}:{}", loc.path, loc.line);
}
}
}
}
fn cmd_stats(path: &Path, custom_cache: Option<&Path>) {
let cache_path = effective_cache_path(path, custom_cache);
let index = load_or_build_index(path, &cache_path);
let (files, defs, refs) = index.stats();
println!("Index Statistics for: {}", path.display());
println!(" Cache location: {}", cache_path.display());
println!(" Files indexed: {}", files);
println!(" Definitions: {}", defs);
println!(" References: {}", refs);
println!(" Aliases: {}", index.alias_count());
let ref_counts = index.top_referenced_symbols(10);
println!("\nTop 10 most referenced symbols:");
for (name, count) in &ref_counts {
println!(" {:6} {}", count, name);
}
}
fn print_json(result: &kigi_codebase_graph::NavigationResult) {
use serde_json::json;
let locations: Vec<_> = result
.locations
.iter()
.map(|loc| {
json!({
"path": &loc.path,
"line": loc.line,
"symbol": loc.symbol,
})
})
.collect();
let output = json!({
"symbol": result.symbol,
"locations": locations,
});
println!("{}", serde_json::to_string_pretty(&output).unwrap());
}