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).
387 lines
11 KiB
Rust
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());
|
|
}
|