Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 55 additions & 19 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,64 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Build & Test Commands

```bash
cargo build # Build the project
cargo test # Run all tests
cargo build # Build entire workspace
cargo build -p cli # Build CLI binary only
cargo build -p db # Build database library only
cargo test # Run all tests in workspace
cargo test -p db # Test database layer only
cargo test -p code_search # Test CLI layer only
cargo test <test_name> # Run a single test by name
cargo nextest run # Alternative test runner (faster)
cargo run -- --help # Show CLI help
cargo run -- describe # Show detailed command documentation
cargo run -p code_search -- --help # Show CLI help
cargo run -p code_search -- describe # Show detailed command documentation
```

## Workspace Structure

This is a Cargo workspace with two crates:

- **`db/`** - Database library crate
- CozoDB query layer (all `queries/` modules)
- Database utilities (`db.rs`)
- Shared types (`types/`)
- Query builders (`query_builders.rs`)
- Test utilities and fixtures (behind `test-utils` feature flag)

- **`cli/`** - CLI binary crate (package name: `code_search`)
- Command-line interface (`cli.rs`, `main.rs`)
- All command modules (`commands/`)
- Output formatting (`output.rs`)
- Presentation utilities (`utils.rs`, `dedup.rs`)
- Test macros (`test_macros.rs`)

**Dependency flow:** `cli` depends on `db` via `db = { path = "../db" }`. The database layer is completely independent of the CLI.

**Test utilities:** Database test helpers and fixtures are available via the `test-utils` feature. CLI tests use: `db = { path = "../db", features = ["test-utils"] }`

## Architecture

This is a Rust CLI tool for querying call graph data stored in a CozoDB SQLite database. Uses Rust 2024 edition with clap derive macros for CLI parsing.

**Code organization:**
- `src/main.rs` - Entry point, module declarations
- `src/cli.rs` - Top-level CLI structure with global `--db` and `--format` flags
- `src/commands/mod.rs` - `Command` enum, `Execute` trait, `CommonArgs`, dispatch via enum_dispatch
- `src/commands/<name>/` - Individual command modules (directory structure)
- `src/queries/<name>.rs` - CozoScript queries and result parsing (separate from command logic)
- `src/db.rs` - Database connection and query utilities
- `src/output.rs` - `OutputFormat` enum, `Outputable` and `TableFormatter` traits
- `src/dedup.rs` - Deduplication utilities (`sort_and_deduplicate`, `DeduplicationFilter`)
- `src/utils.rs` - Module grouping helpers (`group_by_module`, `convert_to_module_groups`)
- `src/types/` - Shared types (`ModuleGroupResult`, `ModuleGroup`, `Call`, etc.)
- `src/test_macros.rs` - Declarative test macros for CLI, execute, and output tests

*Database crate (`db/src/`):*
- `lib.rs` - Public API surface, re-exports
- `db.rs` - Database connection and query utilities
- `queries/<name>.rs` - CozoScript queries and result parsing (31 query modules)
- `query_builders.rs` - SQL condition builders (`ConditionBuilder`, `OptionalConditionBuilder`)
- `types/` - Shared types (`ModuleGroupResult`, `ModuleGroup`, `Call`, `FunctionRef`, etc.)
- `fixtures/` - Test data (feature-gated)
- `test_utils.rs` - Test helpers (feature-gated)

*CLI crate (`cli/src/`):*
- `main.rs` - Entry point, module declarations
- `cli.rs` - Top-level CLI structure with global `--db` and `--format` flags
- `commands/mod.rs` - `Command` enum, `Execute` trait, `CommonArgs`, dispatch via enum_dispatch
- `commands/<name>/` - Individual command modules (27 commands, directory structure)
- `output.rs` - `OutputFormat` enum, `Outputable` and `TableFormatter` traits
- `dedup.rs` - Deduplication utilities (`sort_and_deduplicate`, `DeduplicationFilter`)
- `utils.rs` - Presentation helpers (`group_by_module`, `convert_to_module_groups`, `format_type_definition`)
- `test_macros.rs` - Declarative test macros for CLI, execute, and output tests

**Command module structure:**
Each command is a directory module with these files:
Expand All @@ -39,9 +73,10 @@ Each command is a directory module with these files:

**Execute trait:**
```rust
// Defined in cli/src/commands/mod.rs
pub trait Execute {
type Output: Outputable;
fn execute(self, db: &DbInstance) -> Result<Self::Output, Box<dyn Error>>;
fn execute(self, db: &db::DbInstance) -> Result<Self::Output, Box<dyn Error>>;
}
```

Expand All @@ -67,7 +102,7 @@ pub trait Execute {

When refactoring output, ensure all three formats remain consistent:
1. The struct hierarchy should make sense for both JSON and toon
2. Test fixtures exist in `src/fixtures/output/<command>/` for JSON and toon
2. Test fixtures exist in `db/src/fixtures/output/<command>/` for JSON and toon
3. Output tests verify round-trip consistency between formats

**Dispatch flow:**
Expand Down Expand Up @@ -96,8 +131,9 @@ pub struct MyCmd {
- Uses `tempfile` for filesystem-based tests
- Tests live alongside implementation in each module
- Output tests use expected string constants for clarity
- Test macros in `src/test_macros.rs` reduce boilerplate (see `docs/TESTING_STRATEGY.md`)
- Shared fixtures in `src/fixtures/` for database and output tests
- Test macros in `cli/src/test_macros.rs` reduce boilerplate (see `docs/TESTING_STRATEGY.md`)
- Shared fixtures in `db/src/fixtures/` for database and output tests
- Database test utilities available via `db` crate with `test-utils` feature
- Run with `cargo test` or `cargo nextest run`

**Adding new commands:**
Expand Down
18 changes: 16 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 5 additions & 20 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,22 +1,7 @@
[package]
name = "code_search"
[workspace]
resolver = "2"
members = ["db", "cli"]

[workspace.package]
version = "0.1.0"
edition = "2024"

[dependencies]
clap = { version = "4", features = ["derive"] }
cozo = { version = "0.7.6", default-features = false, features = ["compact", "storage-sqlite"] }
enum_dispatch = "0.3"
thiserror = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toon = "0.1"
regex = "1"
include_dir = "0.7"
home = "0.5.12"

[dev-dependencies]
tempfile = "3"
rstest = "0.23"
serial_test = "3.2.0"

21 changes: 21 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "code_search"
version.workspace = true
edition.workspace = true

[dependencies]
db = { path = "../db" }
clap = { version = "4", features = ["derive"] }
enum_dispatch = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toon = "0.1"
regex = "1"
include_dir = "0.7"
home = "0.5.12"

[dev-dependencies]
db = { path = "../db", features = ["test-utils"] }
tempfile = "3"
rstest = "0.23"
serial_test = "3.2.0"
File renamed without changes.
66 changes: 66 additions & 0 deletions cli/src/commands/accepts/execute.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::error::Error;

use serde::Serialize;

use super::AcceptsCmd;
use crate::commands::Execute;
use db::queries::accepts::{find_accepts, AcceptsEntry};
use db::types::ModuleGroupResult;

/// A function's input type information
#[derive(Debug, Clone, Serialize)]
pub struct AcceptsInfo {
pub name: String,
pub arity: i64,
pub inputs: String,
pub return_type: String,
pub line: i64,
}

fn build_accepts_result(
pattern: String,
module_filter: Option<String>,
entries: Vec<AcceptsEntry>,
) -> ModuleGroupResult<AcceptsInfo> {
let total_items = entries.len();

// Use helper to group by module
let items = crate::utils::group_by_module(entries, |entry| {
let accepts_info = AcceptsInfo {
name: entry.name,
arity: entry.arity,
inputs: entry.inputs_string,
return_type: entry.return_string,
line: entry.line,
};
(entry.module, accepts_info)
});

ModuleGroupResult {
module_pattern: module_filter.unwrap_or_else(|| "*".to_string()),
function_pattern: Some(pattern),
total_items,
items,
}
}

impl Execute for AcceptsCmd {
type Output = ModuleGroupResult<AcceptsInfo>;

fn execute(self, db: &db::DbInstance) -> Result<Self::Output, Box<dyn Error>> {
let entries = find_accepts(
db,
&self.pattern,
&self.common.project,
self.common.regex,
self.module.as_deref(),
self.common.limit,
)?;

Ok(build_accepts_result(
self.pattern,
self.module,
entries,
))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod output;
use std::error::Error;

use clap::Args;
use cozo::DbInstance;
use db::DbInstance;

use crate::commands::{CommandRunner, CommonArgs, Execute};
use crate::output::{OutputFormat, Outputable};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
//! Output formatting for accepts command results.

use crate::output::TableFormatter;
use crate::types::ModuleGroupResult;
use db::types::ModuleGroupResult;
use super::execute::AcceptsInfo;

impl TableFormatter for ModuleGroupResult<AcceptsInfo> {
type Entry = AcceptsInfo;

fn format_header(&self) -> String {
let pattern = self.function_pattern.as_ref().map(|s| s.as_str()).unwrap_or("*");
let pattern = self.function_pattern.as_deref().unwrap_or("*");
format!("Functions accepting \"{}\"", pattern)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use serde::Serialize;

use super::BoundariesCmd;
use crate::commands::Execute;
use crate::queries::hotspots::{find_hotspots, HotspotKind};
use crate::types::{ModuleCollectionResult, ModuleGroup};
use db::queries::hotspots::{find_hotspots, HotspotKind};
use db::types::{ModuleCollectionResult, ModuleGroup};

/// A single boundary module entry
#[derive(Debug, Clone, Serialize)]
Expand All @@ -18,7 +18,7 @@ pub struct BoundaryEntry {
impl Execute for BoundariesCmd {
type Output = ModuleCollectionResult<BoundaryEntry>;

fn execute(self, db: &cozo::DbInstance) -> Result<Self::Output, Box<dyn Error>> {
fn execute(self, db: &db::DbInstance) -> Result<Self::Output, Box<dyn Error>> {
let hotspots = find_hotspots(
db,
HotspotKind::Ratio,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod output;
use std::error::Error;

use clap::Args;
use cozo::DbInstance;
use db::DbInstance;

use crate::commands::{CommandRunner, CommonArgs, Execute};
use crate::output::{OutputFormat, Outputable};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use super::execute::BoundaryEntry;
use crate::output::TableFormatter;
use crate::types::ModuleCollectionResult;
use db::types::ModuleCollectionResult;

impl TableFormatter for ModuleCollectionResult<BoundaryEntry> {
type Entry = BoundaryEntry;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ use serde::Serialize;

use super::{BrowseModuleCmd, DefinitionKind};
use crate::commands::Execute;
use crate::queries::file::find_functions_in_module;
use crate::queries::specs::find_specs;
use crate::queries::types::find_types;
use crate::queries::structs::{find_struct_fields, group_fields_into_structs, FieldInfo};
use db::queries::file::find_functions_in_module;
use db::queries::specs::find_specs;
use db::queries::types::find_types;
use db::queries::structs::{find_struct_fields, group_fields_into_structs, FieldInfo};

/// Result of browsing definitions in a module
#[derive(Debug, Serialize)]
Expand Down Expand Up @@ -119,7 +119,7 @@ impl Definition {
impl Execute for BrowseModuleCmd {
type Output = BrowseModuleResult;

fn execute(self, db: &cozo::DbInstance) -> Result<Self::Output, Box<dyn Error>> {
fn execute(self, db: &db::DbInstance) -> Result<Self::Output, Box<dyn Error>> {
let mut definitions = Vec::new();

// Determine what to query based on kind filter
Expand All @@ -140,11 +140,10 @@ impl Execute for BrowseModuleCmd {

for func in funcs {
// Filter by name if specified
if let Some(ref name_filter) = self.name {
if !func.name.contains(name_filter) {
if let Some(ref name_filter) = self.name
&& !func.name.contains(name_filter) {
continue;
}
}

definitions.push(Definition::Function {
module: func.module,
Expand Down Expand Up @@ -220,11 +219,10 @@ impl Execute for BrowseModuleCmd {

for struct_def in structs {
// Filter by name if specified
if let Some(ref name_filter) = self.name {
if !struct_def.module.contains(name_filter) {
if let Some(ref name_filter) = self.name
&& !struct_def.module.contains(name_filter) {
continue;
}
}

definitions.push(Definition::Struct {
module: struct_def.module.clone(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::error::Error;

use clap::{Parser, ValueEnum};
use cozo::DbInstance;
use db::DbInstance;

use crate::commands::{CommandRunner, CommonArgs, Execute};
use crate::output::{OutputFormat, Outputable};
Expand Down
Loading