diff --git a/CLAUDE.md b/CLAUDE.md index 28240f9..1fdd3a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 # 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//` - Individual command modules (directory structure) -- `src/queries/.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/.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//` - 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: @@ -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>; + fn execute(self, db: &db::DbInstance) -> Result>; } ``` @@ -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//` for JSON and toon +2. Test fixtures exist in `db/src/fixtures/output//` for JSON and toon 3. Output tests verify round-trip consistency between formats **Dispatch flow:** @@ -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:** diff --git a/Cargo.lock b/Cargo.lock index 8396dc1..126cb7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -358,7 +358,7 @@ name = "code_search" version = "0.1.0" dependencies = [ "clap", - "cozo", + "db", "enum_dispatch", "home", "include_dir", @@ -368,7 +368,6 @@ dependencies = [ "serde_json", "serial_test", "tempfile", - "thiserror", "toon", ] @@ -550,6 +549,21 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "db" +version = "0.1.0" +dependencies = [ + "clap", + "cozo", + "include_dir", + "regex", + "rstest", + "serde", + "serde_json", + "tempfile", + "thiserror", +] + [[package]] name = "delegate" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 294354a..1d6d68f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" - diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..cc42df6 --- /dev/null +++ b/cli/Cargo.toml @@ -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" diff --git a/src/cli.rs b/cli/src/cli.rs similarity index 100% rename from src/cli.rs rename to cli/src/cli.rs diff --git a/cli/src/commands/accepts/execute.rs b/cli/src/commands/accepts/execute.rs new file mode 100644 index 0000000..9431f4d --- /dev/null +++ b/cli/src/commands/accepts/execute.rs @@ -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, + entries: Vec, +) -> ModuleGroupResult { + 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; + + fn execute(self, db: &db::DbInstance) -> Result> { + 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, + )) + } +} diff --git a/src/commands/accepts/mod.rs b/cli/src/commands/accepts/mod.rs similarity index 97% rename from src/commands/accepts/mod.rs rename to cli/src/commands/accepts/mod.rs index 7e7c191..623d531 100644 --- a/src/commands/accepts/mod.rs +++ b/cli/src/commands/accepts/mod.rs @@ -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}; diff --git a/src/commands/accepts/output.rs b/cli/src/commands/accepts/output.rs similarity index 88% rename from src/commands/accepts/output.rs rename to cli/src/commands/accepts/output.rs index 9d7e44a..5b0b984 100644 --- a/src/commands/accepts/output.rs +++ b/cli/src/commands/accepts/output.rs @@ -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 { 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) } diff --git a/src/commands/boundaries/execute.rs b/cli/src/commands/boundaries/execute.rs similarity index 93% rename from src/commands/boundaries/execute.rs rename to cli/src/commands/boundaries/execute.rs index 20120a6..09686ab 100644 --- a/src/commands/boundaries/execute.rs +++ b/cli/src/commands/boundaries/execute.rs @@ -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)] @@ -18,7 +18,7 @@ pub struct BoundaryEntry { impl Execute for BoundariesCmd { type Output = ModuleCollectionResult; - fn execute(self, db: &cozo::DbInstance) -> Result> { + fn execute(self, db: &db::DbInstance) -> Result> { let hotspots = find_hotspots( db, HotspotKind::Ratio, diff --git a/src/commands/boundaries/mod.rs b/cli/src/commands/boundaries/mod.rs similarity index 98% rename from src/commands/boundaries/mod.rs rename to cli/src/commands/boundaries/mod.rs index 7f4e01b..09576ab 100644 --- a/src/commands/boundaries/mod.rs +++ b/cli/src/commands/boundaries/mod.rs @@ -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}; diff --git a/src/commands/boundaries/output.rs b/cli/src/commands/boundaries/output.rs similarity index 98% rename from src/commands/boundaries/output.rs rename to cli/src/commands/boundaries/output.rs index 8dc4139..439cf92 100644 --- a/src/commands/boundaries/output.rs +++ b/cli/src/commands/boundaries/output.rs @@ -2,7 +2,7 @@ use super::execute::BoundaryEntry; use crate::output::TableFormatter; -use crate::types::ModuleCollectionResult; +use db::types::ModuleCollectionResult; impl TableFormatter for ModuleCollectionResult { type Entry = BoundaryEntry; diff --git a/src/commands/browse_module/cli_tests.rs b/cli/src/commands/browse_module/cli_tests.rs similarity index 100% rename from src/commands/browse_module/cli_tests.rs rename to cli/src/commands/browse_module/cli_tests.rs diff --git a/src/commands/browse_module/execute.rs b/cli/src/commands/browse_module/execute.rs similarity index 94% rename from src/commands/browse_module/execute.rs rename to cli/src/commands/browse_module/execute.rs index d239845..7714ceb 100644 --- a/src/commands/browse_module/execute.rs +++ b/cli/src/commands/browse_module/execute.rs @@ -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)] @@ -119,7 +119,7 @@ impl Definition { impl Execute for BrowseModuleCmd { type Output = BrowseModuleResult; - fn execute(self, db: &cozo::DbInstance) -> Result> { + fn execute(self, db: &db::DbInstance) -> Result> { let mut definitions = Vec::new(); // Determine what to query based on kind filter @@ -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, @@ -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(), diff --git a/src/commands/browse_module/execute_tests.rs b/cli/src/commands/browse_module/execute_tests.rs similarity index 100% rename from src/commands/browse_module/execute_tests.rs rename to cli/src/commands/browse_module/execute_tests.rs diff --git a/src/commands/browse_module/mod.rs b/cli/src/commands/browse_module/mod.rs similarity index 99% rename from src/commands/browse_module/mod.rs rename to cli/src/commands/browse_module/mod.rs index 791f378..c7cde0c 100644 --- a/src/commands/browse_module/mod.rs +++ b/cli/src/commands/browse_module/mod.rs @@ -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}; diff --git a/src/commands/browse_module/output.rs b/cli/src/commands/browse_module/output.rs similarity index 84% rename from src/commands/browse_module/output.rs rename to cli/src/commands/browse_module/output.rs index 869a53b..e6f0062 100644 --- a/src/commands/browse_module/output.rs +++ b/cli/src/commands/browse_module/output.rs @@ -28,17 +28,14 @@ impl Outputable for BrowseModuleResult { } // Summary - output.push_str(&format!( - "Found {} definition(s):\n\n", - self.total_items - )); + output.push_str(&format!("Found {} definition(s):\n\n", self.total_items)); // Group by module for readability let mut by_module: BTreeMap> = BTreeMap::new(); for def in &self.definitions { by_module .entry(def.module().to_string()) - .or_insert_with(Vec::new) + .or_default() .push(def); } @@ -58,9 +55,14 @@ impl Outputable for BrowseModuleResult { return_type, .. } => { - output.push_str(&format!(" L{}-{} [{}] {}/{}\n", start_line, end_line, kind, name, arity)); + output.push_str(&format!( + " L{}-{} [{}] {}/{}\n", + start_line, end_line, kind, name, arity + )); if !args.is_empty() || !return_type.is_empty() { - output.push_str(&format!(" {} {}\n", args, return_type).trim_end()); + output.push_str( + format!(" {} {}\n", args, return_type).trim_end(), + ); output.push('\n'); } } @@ -73,7 +75,10 @@ impl Outputable for BrowseModuleResult { full, .. } => { - output.push_str(&format!(" L{:<3} [{}] {}/{}\n", line, kind, name, arity)); + output.push_str(&format!( + " L{:<3} [{}] {}/{}\n", + line, kind, name, arity + )); if !full.is_empty() { output.push_str(&format!(" {}\n", full)); } @@ -89,25 +94,28 @@ impl Outputable for BrowseModuleResult { output.push_str(&format!(" L{:<3} [{}] {}\n", line, kind, name)); if !definition.is_empty() { let formatted = format_type_definition(definition); - // Indent multi-line definitions properly - for (i, def_line) in formatted.lines().enumerate() { - if i == 0 { - output.push_str(&format!(" {}\n", def_line)); - } else { - output.push_str(&format!(" {}\n", def_line)); - } + for def_line in formatted.lines() { + output.push_str(&format!(" {}\n", def_line)); } } } Definition::Struct { name, fields, .. } => { - output.push_str(&format!(" [struct] {} with {} fields\n", name, fields.len())); + output.push_str(&format!( + " [struct] {} with {} fields\n", + name, + fields.len() + )); for field in fields.iter() { output.push_str(&format!( " - {}: {} {}\n", field.name, field.inferred_type, - if field.required { "(required)" } else { "(optional)" } + if field.required { + "(required)" + } else { + "(optional)" + } )); } } diff --git a/src/commands/browse_module/output_tests.rs b/cli/src/commands/browse_module/output_tests.rs similarity index 99% rename from src/commands/browse_module/output_tests.rs rename to cli/src/commands/browse_module/output_tests.rs index 2790b4a..425652b 100644 --- a/src/commands/browse_module/output_tests.rs +++ b/cli/src/commands/browse_module/output_tests.rs @@ -4,7 +4,7 @@ mod tests { use super::super::execute::{BrowseModuleResult, Definition}; use super::super::DefinitionKind; - use crate::queries::structs::FieldInfo; + use db::queries::structs::FieldInfo; use rstest::{fixture, rstest}; // ========================================================================= diff --git a/src/commands/calls_from/cli_tests.rs b/cli/src/commands/calls_from/cli_tests.rs similarity index 100% rename from src/commands/calls_from/cli_tests.rs rename to cli/src/commands/calls_from/cli_tests.rs diff --git a/cli/src/commands/calls_from/execute.rs b/cli/src/commands/calls_from/execute.rs new file mode 100644 index 0000000..79a01cd --- /dev/null +++ b/cli/src/commands/calls_from/execute.rs @@ -0,0 +1,98 @@ +use std::error::Error; + +use serde::Serialize; + +use super::CallsFromCmd; +use crate::commands::Execute; +use db::queries::calls_from::find_calls_from; +use db::types::{Call, ModuleGroupResult}; +use crate::utils::group_calls; + +/// A caller function with all its outgoing calls +#[derive(Debug, Clone, Serialize)] +pub struct CallerFunction { + pub name: String, + pub arity: i64, + pub kind: String, + pub start_line: i64, + pub end_line: i64, + pub calls: Vec, +} + +fn build_calls_from_result(module_pattern: String, function_pattern: String, calls: Vec) -> ModuleGroupResult { + let (total_items, items) = group_calls( + calls, + // Group by caller module + |call| call.caller.module.to_string(), + // Key by caller function metadata + |call| CallerFunctionKey { + name: call.caller.name.to_string(), + arity: call.caller.arity, + kind: call.caller.kind.as_deref().unwrap_or("").to_string(), + start_line: call.caller.start_line.unwrap_or(0), + end_line: call.caller.end_line.unwrap_or(0), + }, + // Sort by line number + |a, b| a.line.cmp(&b.line), + // Deduplicate by callee (module, name, arity) + |c| (c.callee.module.to_string(), c.callee.name.to_string(), c.callee.arity), + // Build CallerFunction entry + |key, calls| CallerFunction { + name: key.name, + arity: key.arity, + kind: key.kind, + start_line: key.start_line, + end_line: key.end_line, + calls, + }, + // File tracking strategy: extract from first call in first function + |_module, functions_map| { + functions_map + .values() + .next() + .and_then(|calls| calls.first()) + .and_then(|call| call.caller.file.as_deref()) + .unwrap_or("") + .to_string() + }, + ); + + ModuleGroupResult { + module_pattern, + function_pattern: Some(function_pattern), + total_items, + items, + } +} + +/// Key for grouping by caller function (used internally) +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct CallerFunctionKey { + name: String, + arity: i64, + kind: String, + start_line: i64, + end_line: i64, +} + +impl Execute for CallsFromCmd { + type Output = ModuleGroupResult; + + fn execute(self, db: &db::DbInstance) -> Result> { + let calls = find_calls_from( + db, + &self.module, + self.function.as_deref(), + self.arity, + &self.common.project, + self.common.regex, + self.common.limit, + )?; + + Ok(build_calls_from_result( + self.module, + self.function.unwrap_or_default(), + calls, + )) + } +} diff --git a/src/commands/calls_from/execute_tests.rs b/cli/src/commands/calls_from/execute_tests.rs similarity index 100% rename from src/commands/calls_from/execute_tests.rs rename to cli/src/commands/calls_from/execute_tests.rs diff --git a/src/commands/calls_from/mod.rs b/cli/src/commands/calls_from/mod.rs similarity index 98% rename from src/commands/calls_from/mod.rs rename to cli/src/commands/calls_from/mod.rs index f0713b7..e312c57 100644 --- a/src/commands/calls_from/mod.rs +++ b/cli/src/commands/calls_from/mod.rs @@ -7,7 +7,7 @@ mod output_tests; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, CommonArgs, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/calls_from/output.rs b/cli/src/commands/calls_from/output.rs similarity index 97% rename from src/commands/calls_from/output.rs rename to cli/src/commands/calls_from/output.rs index d0dac92..afe550b 100644 --- a/src/commands/calls_from/output.rs +++ b/cli/src/commands/calls_from/output.rs @@ -1,7 +1,7 @@ //! Output formatting for calls-from command results. use crate::output::TableFormatter; -use crate::types::ModuleGroupResult; +use db::types::ModuleGroupResult; use super::execute::CallerFunction; impl TableFormatter for ModuleGroupResult { diff --git a/cli/src/commands/calls_from/output_tests.rs b/cli/src/commands/calls_from/output_tests.rs new file mode 100644 index 0000000..278d284 --- /dev/null +++ b/cli/src/commands/calls_from/output_tests.rs @@ -0,0 +1,203 @@ +//! Output formatting tests for calls-from command. + +#[cfg(test)] +mod tests { + use super::super::execute::CallerFunction; + use db::types::{Call, FunctionRef, ModuleGroupResult}; + use rstest::{fixture, rstest}; + + // ========================================================================= + // Expected outputs + // ========================================================================= + + const EMPTY_TABLE: &str = "\ +Calls from: MyApp.Accounts.get_user + +No calls found."; + + const SINGLE_TABLE: &str = "\ +Calls from: MyApp.Accounts.get_user + +Found 1 call(s): + +MyApp.Accounts (lib/my_app/accounts.ex) + get_user/1 (10:15) + → @ L12 MyApp.Repo.get/2"; + + const MULTIPLE_TABLE: &str = "\ +Calls from: MyApp.Accounts + +Found 2 call(s): + +MyApp.Accounts (lib/my_app/accounts.ex) + get_user/1 (10:15) + → @ L12 MyApp.Repo.get/2 + list_users/0 (20:25) + → @ L22 MyApp.Repo.all/1"; + + // ========================================================================= + // Fixtures + // ========================================================================= + + #[fixture] + fn empty_result() -> ModuleGroupResult { + ModuleGroupResult { + module_pattern: "MyApp.Accounts".to_string(), + function_pattern: Some("get_user".to_string()), + total_items: 0, + items: vec![], + } + } + + #[fixture] + fn single_result() -> ModuleGroupResult { + use db::types::ModuleGroup; + + let caller_func = CallerFunction { + name: "get_user".to_string(), + arity: 1, + kind: String::new(), + start_line: 10, + end_line: 15, + calls: vec![Call { + caller: FunctionRef::with_definition( + "MyApp.Accounts", + "get_user", + 1, + "", + "lib/my_app/accounts.ex", + 10, + 15, + ), + callee: FunctionRef::new("MyApp.Repo", "get", 2), + line: 12, + call_type: Some("remote".to_string()), + depth: None, + }], + }; + + ModuleGroupResult { + module_pattern: "MyApp.Accounts".to_string(), + function_pattern: Some("get_user".to_string()), + total_items: 1, + items: vec![ModuleGroup { + name: "MyApp.Accounts".to_string(), + file: "lib/my_app/accounts.ex".to_string(), + entries: vec![caller_func], + function_count: None, + }], + } + } + + #[fixture] + fn multiple_result() -> ModuleGroupResult { + use db::types::ModuleGroup; + + let caller_func1 = CallerFunction { + name: "get_user".to_string(), + arity: 1, + kind: String::new(), + start_line: 10, + end_line: 15, + calls: vec![Call { + caller: FunctionRef::with_definition( + "MyApp.Accounts", + "get_user", + 1, + "", + "lib/my_app/accounts.ex", + 10, + 15, + ), + callee: FunctionRef::new("MyApp.Repo", "get", 2), + line: 12, + call_type: Some("remote".to_string()), + depth: None, + }], + }; + + let caller_func2 = CallerFunction { + name: "list_users".to_string(), + arity: 0, + kind: String::new(), + start_line: 20, + end_line: 25, + calls: vec![Call { + caller: FunctionRef::with_definition( + "MyApp.Accounts", + "list_users", + 0, + "", + "lib/my_app/accounts.ex", + 20, + 25, + ), + callee: FunctionRef::new("MyApp.Repo", "all", 1), + line: 22, + call_type: Some("remote".to_string()), + depth: None, + }], + }; + + ModuleGroupResult { + module_pattern: "MyApp.Accounts".to_string(), + function_pattern: None, + total_items: 2, + items: vec![ModuleGroup { + name: "MyApp.Accounts".to_string(), + file: "lib/my_app/accounts.ex".to_string(), + entries: vec![caller_func1, caller_func2], + function_count: None, + }], + } + } + + // ========================================================================= + // Tests + // ========================================================================= + + crate::output_table_test! { + test_name: test_to_table_empty, + fixture: empty_result, + fixture_type: ModuleGroupResult, + expected: EMPTY_TABLE, + } + + crate::output_table_test! { + test_name: test_to_table_single, + fixture: single_result, + fixture_type: ModuleGroupResult, + expected: SINGLE_TABLE, + } + + crate::output_table_test! { + test_name: test_to_table_multiple, + fixture: multiple_result, + fixture_type: ModuleGroupResult, + expected: MULTIPLE_TABLE, + } + + crate::output_table_test! { + test_name: test_format_json, + fixture: single_result, + fixture_type: ModuleGroupResult, + expected: db::test_utils::load_output_fixture("calls_from", "single.json"), + format: Json, + } + + crate::output_table_test! { + test_name: test_format_toon, + fixture: single_result, + fixture_type: ModuleGroupResult, + expected: db::test_utils::load_output_fixture("calls_from", "single.toon"), + format: Toon, + } + + crate::output_table_test! { + test_name: test_format_toon_empty, + fixture: empty_result, + fixture_type: ModuleGroupResult, + expected: db::test_utils::load_output_fixture("calls_from", "empty.toon"), + format: Toon, + } +} diff --git a/src/commands/calls_to/cli_tests.rs b/cli/src/commands/calls_to/cli_tests.rs similarity index 100% rename from src/commands/calls_to/cli_tests.rs rename to cli/src/commands/calls_to/cli_tests.rs diff --git a/cli/src/commands/calls_to/execute.rs b/cli/src/commands/calls_to/execute.rs new file mode 100644 index 0000000..10d7e03 --- /dev/null +++ b/cli/src/commands/calls_to/execute.rs @@ -0,0 +1,86 @@ +use std::error::Error; + +use serde::Serialize; + +use super::CallsToCmd; +use crate::commands::Execute; +use db::queries::calls_to::find_calls_to; +use db::types::{Call, ModuleGroupResult}; +use crate::utils::group_calls; + +/// A callee function (target) with all its callers +#[derive(Debug, Clone, Serialize)] +pub struct CalleeFunction { + pub name: String, + pub arity: i64, + pub callers: Vec, +} + +/// Key for grouping by callee function +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct CalleeFunctionKey { + name: String, + arity: i64, +} + +/// Build grouped result from flat calls +fn build_callee_result(module_pattern: String, function_pattern: String, calls: Vec) -> ModuleGroupResult { + let (total_items, items) = group_calls( + calls, + // Group by callee module + |call| call.callee.module.to_string(), + // Key by callee function metadata + |call| CalleeFunctionKey { + name: call.callee.name.to_string(), + arity: call.callee.arity, + }, + // Sort by caller module, name, arity, then line + |a, b| { + a.caller.module.as_ref().cmp(b.caller.module.as_ref()) + .then_with(|| a.caller.name.as_ref().cmp(b.caller.name.as_ref())) + .then_with(|| a.caller.arity.cmp(&b.caller.arity)) + .then_with(|| a.line.cmp(&b.line)) + }, + // Deduplicate by caller (module, name, arity) + |c| (c.caller.module.to_string(), c.caller.name.to_string(), c.caller.arity), + // Build CalleeFunction entry + |key, callers| CalleeFunction { + name: key.name, + arity: key.arity, + callers, + }, + // File is intentionally empty because callees are the grouping key, + // and a module can be defined across multiple files. The calls themselves + // carry file information where needed. + |_module, _map| String::new(), + ); + + ModuleGroupResult { + module_pattern, + function_pattern: Some(function_pattern), + total_items, + items, + } +} + +impl Execute for CallsToCmd { + type Output = ModuleGroupResult; + + fn execute(self, db: &db::DbInstance) -> Result> { + let calls = find_calls_to( + db, + &self.module, + self.function.as_deref(), + self.arity, + &self.common.project, + self.common.regex, + self.common.limit, + )?; + + Ok(build_callee_result( + self.module, + self.function.unwrap_or_default(), + calls, + )) + } +} diff --git a/src/commands/calls_to/execute_tests.rs b/cli/src/commands/calls_to/execute_tests.rs similarity index 100% rename from src/commands/calls_to/execute_tests.rs rename to cli/src/commands/calls_to/execute_tests.rs diff --git a/src/commands/calls_to/mod.rs b/cli/src/commands/calls_to/mod.rs similarity index 98% rename from src/commands/calls_to/mod.rs rename to cli/src/commands/calls_to/mod.rs index f4d13d0..d159139 100644 --- a/src/commands/calls_to/mod.rs +++ b/cli/src/commands/calls_to/mod.rs @@ -7,7 +7,7 @@ mod output_tests; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, CommonArgs, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/calls_to/output.rs b/cli/src/commands/calls_to/output.rs similarity index 97% rename from src/commands/calls_to/output.rs rename to cli/src/commands/calls_to/output.rs index 2bdbc3d..de2de65 100644 --- a/src/commands/calls_to/output.rs +++ b/cli/src/commands/calls_to/output.rs @@ -1,7 +1,7 @@ //! Output formatting for calls-to command results. use crate::output::TableFormatter; -use crate::types::ModuleGroupResult; +use db::types::ModuleGroupResult; use super::execute::CalleeFunction; impl TableFormatter for ModuleGroupResult { diff --git a/src/commands/calls_to/output_tests.rs b/cli/src/commands/calls_to/output_tests.rs similarity index 71% rename from src/commands/calls_to/output_tests.rs rename to cli/src/commands/calls_to/output_tests.rs index 9e52915..b8e3665 100644 --- a/src/commands/calls_to/output_tests.rs +++ b/cli/src/commands/calls_to/output_tests.rs @@ -3,7 +3,7 @@ #[cfg(test)] mod tests { use super::super::execute::CalleeFunction; - use crate::types::{Call, FunctionRef, ModuleGroupResult}; + use db::types::{Call, FunctionRef, ModuleGroupResult}; use rstest::{fixture, rstest}; // ========================================================================= @@ -41,19 +41,22 @@ MyApp.Repo #[fixture] fn empty_result() -> ModuleGroupResult { - >::from_calls( - "MyApp.Repo".to_string(), - "get".to_string(), - vec![], - ) + ModuleGroupResult { + module_pattern: "MyApp.Repo".to_string(), + function_pattern: Some("get".to_string()), + total_items: 0, + items: vec![], + } } #[fixture] fn single_result() -> ModuleGroupResult { - >::from_calls( - "MyApp.Repo".to_string(), - "get".to_string(), - vec![Call { + use db::types::ModuleGroup; + + let callee_func = CalleeFunction { + name: "get".to_string(), + arity: 2, + callers: vec![Call { caller: FunctionRef::with_definition( "MyApp.Accounts", "get_user", @@ -68,15 +71,29 @@ MyApp.Repo call_type: Some("remote".to_string()), depth: None, }], - ) + }; + + ModuleGroupResult { + module_pattern: "MyApp.Repo".to_string(), + function_pattern: Some("get".to_string()), + total_items: 1, + items: vec![ModuleGroup { + name: "MyApp.Repo".to_string(), + file: String::new(), + entries: vec![callee_func], + function_count: None, + }], + } } #[fixture] fn multiple_result() -> ModuleGroupResult { - >::from_calls( - "MyApp.Repo".to_string(), - String::new(), - vec![ + use db::types::ModuleGroup; + + let callee_func = CalleeFunction { + name: "get".to_string(), + arity: 2, + callers: vec![ Call { caller: FunctionRef::with_definition( "MyApp.Accounts", @@ -108,7 +125,19 @@ MyApp.Repo depth: None, }, ], - ) + }; + + ModuleGroupResult { + module_pattern: "MyApp.Repo".to_string(), + function_pattern: None, + total_items: 2, + items: vec![ModuleGroup { + name: "MyApp.Repo".to_string(), + file: String::new(), + entries: vec![callee_func], + function_count: None, + }], + } } // ========================================================================= @@ -140,7 +169,7 @@ MyApp.Repo test_name: test_format_json, fixture: single_result, fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("calls_to", "single.json"), + expected: db::test_utils::load_output_fixture("calls_to", "single.json"), format: Json, } @@ -148,7 +177,7 @@ MyApp.Repo test_name: test_format_toon, fixture: single_result, fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("calls_to", "single.toon"), + expected: db::test_utils::load_output_fixture("calls_to", "single.toon"), format: Toon, } @@ -156,7 +185,7 @@ MyApp.Repo test_name: test_format_toon_empty, fixture: empty_result, fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("calls_to", "empty.toon"), + expected: db::test_utils::load_output_fixture("calls_to", "empty.toon"), format: Toon, } } diff --git a/src/commands/clusters/execute.rs b/cli/src/commands/clusters/execute.rs similarity index 98% rename from src/commands/clusters/execute.rs rename to cli/src/commands/clusters/execute.rs index 76ddd29..2bf3ec7 100644 --- a/src/commands/clusters/execute.rs +++ b/cli/src/commands/clusters/execute.rs @@ -5,7 +5,7 @@ use serde::Serialize; use super::ClustersCmd; use crate::commands::Execute; -use crate::queries::clusters::get_module_calls; +use db::queries::clusters::get_module_calls; /// A single namespace cluster #[derive(Debug, Clone, Serialize)] @@ -44,7 +44,7 @@ pub struct ClustersResult { impl Execute for ClustersCmd { type Output = ClustersResult; - fn execute(self, db: &cozo::DbInstance) -> Result> { + fn execute(self, db: &db::DbInstance) -> Result> { // Get all inter-module calls let calls = get_module_calls(db, &self.common.project)?; @@ -72,7 +72,7 @@ impl Execute for ClustersCmd { let namespace = extract_namespace(module, self.depth); namespace_modules .entry(namespace) - .or_insert_with(HashSet::new) + .or_default() .insert(module.clone()); } diff --git a/src/commands/clusters/mod.rs b/cli/src/commands/clusters/mod.rs similarity index 98% rename from src/commands/clusters/mod.rs rename to cli/src/commands/clusters/mod.rs index 0c4e1e9..a08a3ea 100644 --- a/src/commands/clusters/mod.rs +++ b/cli/src/commands/clusters/mod.rs @@ -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}; diff --git a/src/commands/clusters/output.rs b/cli/src/commands/clusters/output.rs similarity index 100% rename from src/commands/clusters/output.rs rename to cli/src/commands/clusters/output.rs diff --git a/src/commands/complexity/cli_tests.rs b/cli/src/commands/complexity/cli_tests.rs similarity index 100% rename from src/commands/complexity/cli_tests.rs rename to cli/src/commands/complexity/cli_tests.rs diff --git a/src/commands/complexity/execute.rs b/cli/src/commands/complexity/execute.rs similarity index 92% rename from src/commands/complexity/execute.rs rename to cli/src/commands/complexity/execute.rs index 105742b..e625e30 100644 --- a/src/commands/complexity/execute.rs +++ b/cli/src/commands/complexity/execute.rs @@ -4,8 +4,8 @@ use serde::Serialize; use super::ComplexityCmd; use crate::commands::Execute; -use crate::queries::complexity::find_complexity_metrics; -use crate::types::ModuleCollectionResult; +use db::queries::complexity::find_complexity_metrics; +use db::types::ModuleCollectionResult; /// A single complexity metric entry #[derive(Debug, Clone, Serialize)] @@ -21,7 +21,7 @@ pub struct ComplexityEntry { impl Execute for ComplexityCmd { type Output = ModuleCollectionResult; - fn execute(self, db: &cozo::DbInstance) -> Result> { + fn execute(self, db: &db::DbInstance) -> Result> { let metrics = find_complexity_metrics( db, self.min, diff --git a/src/commands/complexity/execute_tests.rs b/cli/src/commands/complexity/execute_tests.rs similarity index 100% rename from src/commands/complexity/execute_tests.rs rename to cli/src/commands/complexity/execute_tests.rs diff --git a/src/commands/complexity/mod.rs b/cli/src/commands/complexity/mod.rs similarity index 98% rename from src/commands/complexity/mod.rs rename to cli/src/commands/complexity/mod.rs index d8e7d23..9c9a7eb 100644 --- a/src/commands/complexity/mod.rs +++ b/cli/src/commands/complexity/mod.rs @@ -11,7 +11,7 @@ mod output_tests; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, CommonArgs, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/complexity/output.rs b/cli/src/commands/complexity/output.rs similarity index 96% rename from src/commands/complexity/output.rs rename to cli/src/commands/complexity/output.rs index 0acc4dc..1aa9eea 100644 --- a/src/commands/complexity/output.rs +++ b/cli/src/commands/complexity/output.rs @@ -2,7 +2,7 @@ use super::execute::ComplexityEntry; use crate::output::TableFormatter; -use crate::types::ModuleCollectionResult; +use db::types::ModuleCollectionResult; impl TableFormatter for ModuleCollectionResult { type Entry = ComplexityEntry; diff --git a/src/commands/complexity/output_tests.rs b/cli/src/commands/complexity/output_tests.rs similarity index 98% rename from src/commands/complexity/output_tests.rs rename to cli/src/commands/complexity/output_tests.rs index 47b6d01..87296a0 100644 --- a/src/commands/complexity/output_tests.rs +++ b/cli/src/commands/complexity/output_tests.rs @@ -4,7 +4,7 @@ mod tests { use super::super::execute::ComplexityEntry; use crate::output::Outputable; - use crate::types::{ModuleCollectionResult, ModuleGroup}; + use db::types::{ModuleCollectionResult, ModuleGroup}; #[test] fn test_format_table_single_function() { diff --git a/src/commands/cycles/execute.rs b/cli/src/commands/cycles/execute.rs similarity index 96% rename from src/commands/cycles/execute.rs rename to cli/src/commands/cycles/execute.rs index 9eb497c..cda8cda 100644 --- a/src/commands/cycles/execute.rs +++ b/cli/src/commands/cycles/execute.rs @@ -7,7 +7,7 @@ use serde::Serialize; use super::CyclesCmd; use crate::commands::Execute; -use crate::queries::cycles::find_cycle_edges; +use db::queries::cycles::find_cycle_edges; /// A single cycle found in the module dependency graph #[derive(Debug, Clone, Serialize)] @@ -32,7 +32,7 @@ pub struct CyclesResult { impl Execute for CyclesCmd { type Output = CyclesResult; - fn execute(self, db: &cozo::DbInstance) -> Result> { + fn execute(self, db: &db::DbInstance) -> Result> { // Get cycle edges from the database let edges = find_cycle_edges( db, @@ -55,7 +55,7 @@ impl Execute for CyclesCmd { for edge in &edges { graph .entry(edge.from.clone()) - .or_insert_with(Vec::new) + .or_default() .push(edge.to.clone()); all_modules.insert(edge.from.clone()); all_modules.insert(edge.to.clone()); @@ -96,7 +96,7 @@ fn find_all_cycles(graph: &HashMap>, all_modules: &HashSet, - visited: &mut HashSet, ) -> Vec { let mut cycles = Vec::new(); let mut new_path = path.clone(); @@ -120,7 +119,7 @@ fn dfs_find_cycles( if current == start && !path.is_empty() { // Only report if we haven't already found this cycle // (cycles of length > 1 where start != first path node) - if path.len() > 0 { + if !path.is_empty() { cycles.push(Cycle { length: new_path.len() - 1, // Don't count the repeated start node modules: path.clone(), @@ -137,7 +136,7 @@ fn dfs_find_cycles( // Explore neighbors if let Some(neighbors) = graph.get(current) { for neighbor in neighbors { - let found = dfs_find_cycles(graph, neighbor, start, new_path.clone(), visited); + let found = dfs_find_cycles(graph, neighbor, start, new_path.clone()); cycles.extend(found); } } diff --git a/src/commands/cycles/mod.rs b/cli/src/commands/cycles/mod.rs similarity index 98% rename from src/commands/cycles/mod.rs rename to cli/src/commands/cycles/mod.rs index c1b96c7..e9ff284 100644 --- a/src/commands/cycles/mod.rs +++ b/cli/src/commands/cycles/mod.rs @@ -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}; diff --git a/src/commands/cycles/output.rs b/cli/src/commands/cycles/output.rs similarity index 100% rename from src/commands/cycles/output.rs rename to cli/src/commands/cycles/output.rs diff --git a/src/commands/depended_by/cli_tests.rs b/cli/src/commands/depended_by/cli_tests.rs similarity index 100% rename from src/commands/depended_by/cli_tests.rs rename to cli/src/commands/depended_by/cli_tests.rs diff --git a/cli/src/commands/depended_by/execute.rs b/cli/src/commands/depended_by/execute.rs new file mode 100644 index 0000000..765bc03 --- /dev/null +++ b/cli/src/commands/depended_by/execute.rs @@ -0,0 +1,125 @@ +use std::collections::BTreeMap; +use std::error::Error; + +use serde::Serialize; + +use super::DependedByCmd; +use crate::commands::Execute; +use db::queries::depended_by::find_dependents; +use db::types::{Call, ModuleGroupResult, ModuleGroup}; + +/// A target function being called in the dependency module +#[derive(Debug, Clone, Serialize)] +pub struct DependentTarget { + pub function: String, + pub arity: i64, + pub line: i64, +} + +/// A caller function in a dependent module +#[derive(Debug, Clone, Serialize)] +pub struct DependentCaller { + pub function: String, + pub arity: i64, + pub kind: String, + pub start_line: i64, + pub end_line: i64, + pub file: String, + pub targets: Vec, +} + +/// Build a grouped structure from flat calls +fn build_dependent_caller_result(target_module: String, calls: Vec) -> ModuleGroupResult { + let total_items = calls.len(); + + if calls.is_empty() { + return ModuleGroupResult { + module_pattern: target_module, + function_pattern: None, + total_items: 0, + items: vec![], + }; + } + + // Group by caller_module -> caller_function -> targets + // Using BTreeMap for automatic sorting by module and function key + let mut by_module: BTreeMap>> = BTreeMap::new(); + for call in &calls { + by_module + .entry(call.caller.module.to_string()) + .or_default() + .entry((call.caller.name.to_string(), call.caller.arity)) + .or_default() + .push(call); + } + + let items: Vec> = by_module + .into_iter() + .map(|(module_name, callers_map)| { + // Determine module file from first caller in first function + let module_file = callers_map + .values() + .next() + .and_then(|calls| calls.first()) + .and_then(|call| call.caller.file.as_deref()) + .unwrap_or("") + .to_string(); + + let entries: Vec = callers_map + .into_iter() + .map(|((func_name, arity), func_calls)| { + let first = func_calls[0]; + + let targets: Vec = func_calls + .iter() + .map(|c| DependentTarget { + function: c.callee.name.to_string(), + arity: c.callee.arity, + line: c.line, + }) + .collect(); + + DependentCaller { + function: func_name, + arity, + kind: first.caller.kind.as_deref().unwrap_or("").to_string(), + start_line: first.caller.start_line.unwrap_or(0), + end_line: first.caller.end_line.unwrap_or(0), + file: first.caller.file.as_deref().unwrap_or("").to_string(), + targets, + } + }) + .collect(); + + ModuleGroup { + name: module_name, + file: module_file, + entries, + function_count: None, + } + }) + .collect(); + + ModuleGroupResult { + module_pattern: target_module, + function_pattern: None, + total_items, + items, + } +} + +impl Execute for DependedByCmd { + type Output = ModuleGroupResult; + + fn execute(self, db: &db::DbInstance) -> Result> { + let calls = find_dependents( + db, + &self.module, + &self.common.project, + self.common.regex, + self.common.limit, + )?; + + Ok(build_dependent_caller_result(self.module, calls)) + } +} diff --git a/src/commands/depended_by/execute_tests.rs b/cli/src/commands/depended_by/execute_tests.rs similarity index 100% rename from src/commands/depended_by/execute_tests.rs rename to cli/src/commands/depended_by/execute_tests.rs diff --git a/src/commands/depended_by/mod.rs b/cli/src/commands/depended_by/mod.rs similarity index 97% rename from src/commands/depended_by/mod.rs rename to cli/src/commands/depended_by/mod.rs index d52f684..3dcc2ce 100644 --- a/src/commands/depended_by/mod.rs +++ b/cli/src/commands/depended_by/mod.rs @@ -7,7 +7,7 @@ mod output_tests; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, CommonArgs, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/depended_by/output.rs b/cli/src/commands/depended_by/output.rs similarity index 97% rename from src/commands/depended_by/output.rs rename to cli/src/commands/depended_by/output.rs index abb7081..0949fb3 100644 --- a/src/commands/depended_by/output.rs +++ b/cli/src/commands/depended_by/output.rs @@ -1,7 +1,7 @@ //! Output formatting for depended-by command results. use crate::output::TableFormatter; -use crate::types::ModuleGroupResult; +use db::types::ModuleGroupResult; use super::execute::DependentCaller; impl TableFormatter for ModuleGroupResult { diff --git a/src/commands/depended_by/output_tests.rs b/cli/src/commands/depended_by/output_tests.rs similarity index 94% rename from src/commands/depended_by/output_tests.rs rename to cli/src/commands/depended_by/output_tests.rs index d1a27d7..a595efa 100644 --- a/src/commands/depended_by/output_tests.rs +++ b/cli/src/commands/depended_by/output_tests.rs @@ -3,7 +3,7 @@ #[cfg(test)] mod tests { use super::super::execute::{DependentCaller, DependentTarget}; - use crate::types::{ModuleGroupResult, ModuleGroup}; + use db::types::{ModuleGroupResult, ModuleGroup}; use rstest::{fixture, rstest}; // ========================================================================= @@ -154,7 +154,7 @@ MyApp.Service: test_name: test_format_json, fixture: single_result, fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("depended_by", "single.json"), + expected: db::test_utils::load_output_fixture("depended_by", "single.json"), format: Json, } @@ -162,7 +162,7 @@ MyApp.Service: test_name: test_format_toon, fixture: single_result, fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("depended_by", "single.toon"), + expected: db::test_utils::load_output_fixture("depended_by", "single.toon"), format: Toon, } @@ -170,7 +170,7 @@ MyApp.Service: test_name: test_format_toon_empty, fixture: empty_result, fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("depended_by", "empty.toon"), + expected: db::test_utils::load_output_fixture("depended_by", "empty.toon"), format: Toon, } } diff --git a/src/commands/depends_on/cli_tests.rs b/cli/src/commands/depends_on/cli_tests.rs similarity index 100% rename from src/commands/depends_on/cli_tests.rs rename to cli/src/commands/depends_on/cli_tests.rs diff --git a/cli/src/commands/depends_on/execute.rs b/cli/src/commands/depends_on/execute.rs new file mode 100644 index 0000000..a5c3223 --- /dev/null +++ b/cli/src/commands/depends_on/execute.rs @@ -0,0 +1,81 @@ +use std::collections::BTreeMap; +use std::error::Error; + +use serde::Serialize; + +use super::DependsOnCmd; +use crate::commands::Execute; +use db::queries::depends_on::find_dependencies; +use db::types::{Call, ModuleGroupResult}; +use crate::utils::convert_to_module_groups; + +/// A function in a dependency module being called +#[derive(Debug, Clone, Serialize)] +pub struct DependencyFunction { + pub name: String, + pub arity: i64, + pub callers: Vec, +} + +/// Build a grouped structure from flat calls +fn build_dependency_result(source_module: String, calls: Vec) -> ModuleGroupResult { + let total_items = calls.len(); + + if calls.is_empty() { + return ModuleGroupResult { + module_pattern: source_module, + function_pattern: None, + total_items: 0, + items: vec![], + }; + } + + // Group by callee_module -> callee_function -> callers + // Using BTreeMap for automatic sorting + let mut by_module: BTreeMap>> = BTreeMap::new(); + for call in calls { + by_module + .entry(call.callee.module.to_string()) + .or_default() + .entry((call.callee.name.to_string(), call.callee.arity)) + .or_default() + .push(call); + } + + // Convert to ModuleGroup structure + let items = convert_to_module_groups( + by_module, + |(func_name, arity), callers| DependencyFunction { + name: func_name, + arity, + callers, + }, + // File is intentionally empty because dependencies are the grouping key, + // and a module can depend on functions defined across multiple files. + // The dependency targets themselves carry file information where needed. + |_module, _map| String::new(), + ); + + ModuleGroupResult { + module_pattern: source_module, + function_pattern: None, + total_items, + items, + } +} + +impl Execute for DependsOnCmd { + type Output = ModuleGroupResult; + + fn execute(self, db: &db::DbInstance) -> Result> { + let calls = find_dependencies( + db, + &self.module, + &self.common.project, + self.common.regex, + self.common.limit, + )?; + + Ok(build_dependency_result(self.module, calls)) + } +} diff --git a/src/commands/depends_on/execute_tests.rs b/cli/src/commands/depends_on/execute_tests.rs similarity index 100% rename from src/commands/depends_on/execute_tests.rs rename to cli/src/commands/depends_on/execute_tests.rs diff --git a/src/commands/depends_on/mod.rs b/cli/src/commands/depends_on/mod.rs similarity index 97% rename from src/commands/depends_on/mod.rs rename to cli/src/commands/depends_on/mod.rs index ea72d6b..2724678 100644 --- a/src/commands/depends_on/mod.rs +++ b/cli/src/commands/depends_on/mod.rs @@ -7,7 +7,7 @@ mod output_tests; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, CommonArgs, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/depends_on/output.rs b/cli/src/commands/depends_on/output.rs similarity index 96% rename from src/commands/depends_on/output.rs rename to cli/src/commands/depends_on/output.rs index 784e713..af61563 100644 --- a/src/commands/depends_on/output.rs +++ b/cli/src/commands/depends_on/output.rs @@ -1,7 +1,7 @@ //! Output formatting for depends-on command results. use crate::output::TableFormatter; -use crate::types::ModuleGroupResult; +use db::types::ModuleGroupResult; use super::execute::DependencyFunction; impl TableFormatter for ModuleGroupResult { diff --git a/src/commands/depends_on/output_tests.rs b/cli/src/commands/depends_on/output_tests.rs similarity index 94% rename from src/commands/depends_on/output_tests.rs rename to cli/src/commands/depends_on/output_tests.rs index 7c74549..006e8a2 100644 --- a/src/commands/depends_on/output_tests.rs +++ b/cli/src/commands/depends_on/output_tests.rs @@ -3,7 +3,7 @@ #[cfg(test)] mod tests { use super::super::execute::DependencyFunction; - use crate::types::{Call, FunctionRef, ModuleGroupResult, ModuleGroup}; + use db::types::{Call, FunctionRef, ModuleGroupResult, ModuleGroup}; use rstest::{fixture, rstest}; // ========================================================================= @@ -172,7 +172,7 @@ Phoenix.View: test_name: test_format_json, fixture: single_result, fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("depends_on", "single.json"), + expected: db::test_utils::load_output_fixture("depends_on", "single.json"), format: Json, } @@ -180,7 +180,7 @@ Phoenix.View: test_name: test_format_toon, fixture: single_result, fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("depends_on", "single.toon"), + expected: db::test_utils::load_output_fixture("depends_on", "single.toon"), format: Toon, } @@ -188,7 +188,7 @@ Phoenix.View: test_name: test_format_toon_empty, fixture: empty_result, fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("depends_on", "empty.toon"), + expected: db::test_utils::load_output_fixture("depends_on", "empty.toon"), format: Toon, } } diff --git a/src/commands/describe/descriptions.rs b/cli/src/commands/describe/descriptions.rs similarity index 100% rename from src/commands/describe/descriptions.rs rename to cli/src/commands/describe/descriptions.rs diff --git a/src/commands/describe/execute.rs b/cli/src/commands/describe/execute.rs similarity index 97% rename from src/commands/describe/execute.rs rename to cli/src/commands/describe/execute.rs index 2e2db5a..4b45b7f 100644 --- a/src/commands/describe/execute.rs +++ b/cli/src/commands/describe/execute.rs @@ -34,7 +34,7 @@ pub struct DescribeResult { impl Execute for DescribeCmd { type Output = DescribeResult; - fn execute(self, _db: &cozo::DbInstance) -> Result> { + fn execute(self, _db: &db::DbInstance) -> Result> { if self.commands.is_empty() { // List all commands grouped by category let categories_map = descriptions_by_category(); diff --git a/src/commands/describe/mod.rs b/cli/src/commands/describe/mod.rs similarity index 97% rename from src/commands/describe/mod.rs rename to cli/src/commands/describe/mod.rs index f80a258..18e1fa1 100644 --- a/src/commands/describe/mod.rs +++ b/cli/src/commands/describe/mod.rs @@ -5,7 +5,7 @@ mod output; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/describe/output.rs b/cli/src/commands/describe/output.rs similarity index 94% rename from src/commands/describe/output.rs rename to cli/src/commands/describe/output.rs index 7353376..07b13af 100644 --- a/src/commands/describe/output.rs +++ b/cli/src/commands/describe/output.rs @@ -16,14 +16,14 @@ impl Outputable for DescribeResult { fn format_list_all(categories: &[CategoryListing]) -> String { let mut output = String::new(); output.push_str("Available Commands\n"); - output.push_str("\n"); + output.push('\n'); for category in categories { output.push_str(&format!("{}:\n", category.category)); for (name, brief) in &category.commands { output.push_str(&format!(" {:<20} {}\n", name, brief)); } - output.push_str("\n"); + output.push('\n'); } output.push_str("Use 'code_search describe ' for detailed information.\n"); @@ -35,24 +35,24 @@ fn format_specific(descriptions: &[CommandDescription]) -> String { for (i, desc) in descriptions.iter().enumerate() { if i > 0 { - output.push_str("\n"); + output.push('\n'); output.push_str("================================================================================\n"); - output.push_str("\n"); + output.push('\n'); } // Title output.push_str(&format!("{} - {}\n", desc.name, desc.brief)); - output.push_str("\n"); + output.push('\n'); // Description output.push_str("DESCRIPTION\n"); output.push_str(&format!(" {}\n", desc.description)); - output.push_str("\n"); + output.push('\n'); // Usage output.push_str("USAGE\n"); output.push_str(&format!(" {}\n", desc.usage)); - output.push_str("\n"); + output.push('\n'); // Examples if !desc.examples.is_empty() { @@ -60,7 +60,7 @@ fn format_specific(descriptions: &[CommandDescription]) -> String { for example in &desc.examples { output.push_str(&format!(" # {}\n", example.description)); output.push_str(&format!(" {}\n", example.command)); - output.push_str("\n"); + output.push('\n'); } } @@ -70,7 +70,7 @@ fn format_specific(descriptions: &[CommandDescription]) -> String { for related in &desc.related { output.push_str(&format!(" {}\n", related)); } - output.push_str("\n"); + output.push('\n'); } } diff --git a/src/commands/duplicates/cli_tests.rs b/cli/src/commands/duplicates/cli_tests.rs similarity index 100% rename from src/commands/duplicates/cli_tests.rs rename to cli/src/commands/duplicates/cli_tests.rs diff --git a/src/commands/duplicates/execute.rs b/cli/src/commands/duplicates/execute.rs similarity index 95% rename from src/commands/duplicates/execute.rs rename to cli/src/commands/duplicates/execute.rs index 8428e2a..e6a5563 100644 --- a/src/commands/duplicates/execute.rs +++ b/cli/src/commands/duplicates/execute.rs @@ -5,7 +5,7 @@ use serde::Serialize; use super::DuplicatesCmd; use crate::commands::Execute; -use crate::queries::duplicates::find_duplicates; +use db::queries::duplicates::find_duplicates; // ============================================================================= // Detailed mode types (default) @@ -83,7 +83,7 @@ pub enum DuplicatesOutput { impl Execute for DuplicatesCmd { type Output = DuplicatesOutput; - fn execute(self, db: &cozo::DbInstance) -> Result> { + fn execute(self, db: &db::DbInstance) -> Result> { let functions = find_duplicates( db, &self.common.project, @@ -102,7 +102,7 @@ impl Execute for DuplicatesCmd { } fn build_detailed_result( - functions: Vec, + functions: Vec, ) -> DuplicatesResult { // Group by hash let mut groups_map: BTreeMap> = BTreeMap::new(); @@ -134,7 +134,7 @@ fn build_detailed_result( } fn build_by_module_result( - functions: Vec, + functions: Vec, ) -> DuplicatesByModuleResult { // Group by module first let mut module_map: BTreeMap> = BTreeMap::new(); diff --git a/src/commands/duplicates/execute_tests.rs b/cli/src/commands/duplicates/execute_tests.rs similarity index 100% rename from src/commands/duplicates/execute_tests.rs rename to cli/src/commands/duplicates/execute_tests.rs diff --git a/src/commands/duplicates/mod.rs b/cli/src/commands/duplicates/mod.rs similarity index 98% rename from src/commands/duplicates/mod.rs rename to cli/src/commands/duplicates/mod.rs index 08940b7..d4caa85 100644 --- a/src/commands/duplicates/mod.rs +++ b/cli/src/commands/duplicates/mod.rs @@ -7,7 +7,7 @@ mod output_tests; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, CommonArgs, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/duplicates/output.rs b/cli/src/commands/duplicates/output.rs similarity index 100% rename from src/commands/duplicates/output.rs rename to cli/src/commands/duplicates/output.rs diff --git a/src/commands/duplicates/output_tests.rs b/cli/src/commands/duplicates/output_tests.rs similarity index 100% rename from src/commands/duplicates/output_tests.rs rename to cli/src/commands/duplicates/output_tests.rs diff --git a/src/commands/function/cli_tests.rs b/cli/src/commands/function/cli_tests.rs similarity index 100% rename from src/commands/function/cli_tests.rs rename to cli/src/commands/function/cli_tests.rs diff --git a/cli/src/commands/function/execute.rs b/cli/src/commands/function/execute.rs new file mode 100644 index 0000000..352916b --- /dev/null +++ b/cli/src/commands/function/execute.rs @@ -0,0 +1,71 @@ +use std::error::Error; + +use serde::Serialize; + +use super::FunctionCmd; +use crate::commands::Execute; +use db::queries::function::{find_functions, FunctionSignature}; +use db::types::ModuleGroupResult; + +/// A function signature within a module +#[derive(Debug, Clone, Serialize)] +pub struct FuncSig { + pub name: String, + pub arity: i64, + #[serde(skip_serializing_if = "String::is_empty")] + pub args: String, + #[serde(skip_serializing_if = "String::is_empty")] + pub return_type: String, +} + +/// Build grouped result from flat FunctionSignature list +fn build_function_signatures_result( + module_pattern: String, + function_pattern: String, + signatures: Vec, +) -> ModuleGroupResult { + let total_items = signatures.len(); + + // Use helper to group by module + let items = crate::utils::group_by_module(signatures, |sig| { + let func_sig = FuncSig { + name: sig.name, + arity: sig.arity, + args: sig.args, + return_type: sig.return_type, + }; + // File is intentionally empty for functions because the function command + // queries the functions table which doesn't track file locations. + // File locations are available in function_locations table if needed. + (sig.module, func_sig) + }); + + ModuleGroupResult { + module_pattern, + function_pattern: Some(function_pattern), + total_items, + items, + } +} + +impl Execute for FunctionCmd { + type Output = ModuleGroupResult; + + fn execute(self, db: &db::DbInstance) -> Result> { + let signatures = find_functions( + db, + &self.module, + &self.function, + self.arity, + &self.common.project, + self.common.regex, + self.common.limit, + )?; + + Ok(build_function_signatures_result( + self.module, + self.function, + signatures, + )) + } +} diff --git a/src/commands/function/execute_tests.rs b/cli/src/commands/function/execute_tests.rs similarity index 100% rename from src/commands/function/execute_tests.rs rename to cli/src/commands/function/execute_tests.rs diff --git a/src/commands/function/mod.rs b/cli/src/commands/function/mod.rs similarity index 98% rename from src/commands/function/mod.rs rename to cli/src/commands/function/mod.rs index a2b21e5..632008c 100644 --- a/src/commands/function/mod.rs +++ b/cli/src/commands/function/mod.rs @@ -7,7 +7,7 @@ mod output_tests; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, CommonArgs, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/function/output.rs b/cli/src/commands/function/output.rs similarity index 90% rename from src/commands/function/output.rs rename to cli/src/commands/function/output.rs index dffa742..8e6b987 100644 --- a/src/commands/function/output.rs +++ b/cli/src/commands/function/output.rs @@ -1,14 +1,14 @@ //! Output formatting for function command results. use crate::output::TableFormatter; -use crate::types::ModuleGroupResult; +use db::types::ModuleGroupResult; use super::execute::FuncSig; impl TableFormatter for ModuleGroupResult { type Entry = FuncSig; fn format_header(&self) -> String { - let function_pattern = self.function_pattern.as_ref().map(|s| s.as_str()).unwrap_or("*"); + let function_pattern = self.function_pattern.as_deref().unwrap_or("*"); format!("Function: {}.{}", self.module_pattern, function_pattern) } diff --git a/src/commands/function/output_tests.rs b/cli/src/commands/function/output_tests.rs similarity index 93% rename from src/commands/function/output_tests.rs rename to cli/src/commands/function/output_tests.rs index 86aec7f..a6b5f81 100644 --- a/src/commands/function/output_tests.rs +++ b/cli/src/commands/function/output_tests.rs @@ -3,7 +3,7 @@ #[cfg(test)] mod tests { use super::super::execute::FuncSig; - use crate::types::{ModuleGroupResult, ModuleGroup}; + use db::types::{ModuleGroupResult, ModuleGroup}; use rstest::{fixture, rstest}; // ========================================================================= @@ -130,7 +130,7 @@ MyApp.Accounts: test_name: test_format_json, fixture: single_result, fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("function", "single.json"), + expected: db::test_utils::load_output_fixture("function", "single.json"), format: Json, } @@ -138,7 +138,7 @@ MyApp.Accounts: test_name: test_format_toon, fixture: single_result, fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("function", "single.toon"), + expected: db::test_utils::load_output_fixture("function", "single.toon"), format: Toon, } @@ -146,7 +146,7 @@ MyApp.Accounts: test_name: test_format_toon_empty, fixture: empty_result, fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("function", "empty.toon"), + expected: db::test_utils::load_output_fixture("function", "empty.toon"), format: Toon, } } diff --git a/src/commands/god_modules/execute.rs b/cli/src/commands/god_modules/execute.rs similarity index 95% rename from src/commands/god_modules/execute.rs rename to cli/src/commands/god_modules/execute.rs index cf4ee0d..f3ca9e7 100644 --- a/src/commands/god_modules/execute.rs +++ b/cli/src/commands/god_modules/execute.rs @@ -4,8 +4,8 @@ use serde::Serialize; use super::GodModulesCmd; use crate::commands::Execute; -use crate::queries::hotspots::{find_hotspots, get_function_counts, get_module_loc, HotspotKind}; -use crate::types::{ModuleCollectionResult, ModuleGroup}; +use db::queries::hotspots::{find_hotspots, get_function_counts, get_module_loc, HotspotKind}; +use db::types::{ModuleCollectionResult, ModuleGroup}; /// A single god module entry #[derive(Debug, Clone, Serialize)] @@ -20,7 +20,7 @@ pub struct GodModuleEntry { impl Execute for GodModulesCmd { type Output = ModuleCollectionResult; - fn execute(self, db: &cozo::DbInstance) -> Result> { + fn execute(self, db: &db::DbInstance) -> Result> { // Get function counts for all modules let func_counts = get_function_counts( db, diff --git a/src/commands/god_modules/mod.rs b/cli/src/commands/god_modules/mod.rs similarity index 98% rename from src/commands/god_modules/mod.rs rename to cli/src/commands/god_modules/mod.rs index 809b4f6..b62914e 100644 --- a/src/commands/god_modules/mod.rs +++ b/cli/src/commands/god_modules/mod.rs @@ -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}; diff --git a/src/commands/god_modules/output.rs b/cli/src/commands/god_modules/output.rs similarity index 97% rename from src/commands/god_modules/output.rs rename to cli/src/commands/god_modules/output.rs index 678d811..5a6c65e 100644 --- a/src/commands/god_modules/output.rs +++ b/cli/src/commands/god_modules/output.rs @@ -2,7 +2,7 @@ use super::execute::GodModuleEntry; use crate::output::TableFormatter; -use crate::types::ModuleCollectionResult; +use db::types::ModuleCollectionResult; impl TableFormatter for ModuleCollectionResult { type Entry = GodModuleEntry; diff --git a/src/commands/hotspots/cli_tests.rs b/cli/src/commands/hotspots/cli_tests.rs similarity index 98% rename from src/commands/hotspots/cli_tests.rs rename to cli/src/commands/hotspots/cli_tests.rs index 6ff3c65..0ca8f1e 100644 --- a/src/commands/hotspots/cli_tests.rs +++ b/cli/src/commands/hotspots/cli_tests.rs @@ -3,7 +3,7 @@ #[cfg(test)] mod tests { use crate::cli::Args; - use crate::queries::hotspots::HotspotKind; + use db::queries::hotspots::HotspotKind; use clap::Parser; use rstest::rstest; diff --git a/src/commands/hotspots/execute.rs b/cli/src/commands/hotspots/execute.rs similarity index 89% rename from src/commands/hotspots/execute.rs rename to cli/src/commands/hotspots/execute.rs index 5d8ca38..bafb3cb 100644 --- a/src/commands/hotspots/execute.rs +++ b/cli/src/commands/hotspots/execute.rs @@ -5,7 +5,7 @@ use serde::Serialize; use super::HotspotsCmd; use crate::commands::Execute; use crate::output::Outputable; -use crate::queries::hotspots::find_hotspots; +use db::queries::hotspots::find_hotspots; /// A function hotspot entry #[derive(Debug, Clone, Serialize)] @@ -100,7 +100,7 @@ impl Outputable for HotspotsResult { impl Execute for HotspotsCmd { type Output = HotspotsResult; - fn execute(self, db: &cozo::DbInstance) -> Result> { + fn execute(self, db: &db::DbInstance) -> Result> { let hotspots = find_hotspots( db, self.kind, @@ -113,10 +113,10 @@ impl Execute for HotspotsCmd { )?; let kind_str = match self.kind { - crate::queries::hotspots::HotspotKind::Incoming => "incoming", - crate::queries::hotspots::HotspotKind::Outgoing => "outgoing", - crate::queries::hotspots::HotspotKind::Total => "total", - crate::queries::hotspots::HotspotKind::Ratio => "ratio", + db::queries::hotspots::HotspotKind::Incoming => "incoming", + db::queries::hotspots::HotspotKind::Outgoing => "outgoing", + db::queries::hotspots::HotspotKind::Total => "total", + db::queries::hotspots::HotspotKind::Ratio => "ratio", }; let entries: Vec = hotspots diff --git a/src/commands/hotspots/execute_tests.rs b/cli/src/commands/hotspots/execute_tests.rs similarity index 90% rename from src/commands/hotspots/execute_tests.rs rename to cli/src/commands/hotspots/execute_tests.rs index 38af50b..ae25895 100644 --- a/src/commands/hotspots/execute_tests.rs +++ b/cli/src/commands/hotspots/execute_tests.rs @@ -5,7 +5,7 @@ mod tests { use super::super::HotspotsCmd; use crate::commands::CommonArgs; use crate::commands::Execute; - use crate::queries::hotspots::HotspotKind; + use db::queries::hotspots::HotspotKind; use rstest::{fixture, rstest}; crate::shared_fixture! { @@ -19,7 +19,7 @@ mod tests { // ========================================================================= #[rstest] - fn test_hotspots_incoming(populated_db: cozo::DbInstance) { + fn test_hotspots_incoming(populated_db: db::DbInstance) { let cmd = HotspotsCmd { module: None, kind: HotspotKind::Incoming, @@ -37,7 +37,7 @@ mod tests { } #[rstest] - fn test_hotspots_outgoing(populated_db: cozo::DbInstance) { + fn test_hotspots_outgoing(populated_db: db::DbInstance) { let cmd = HotspotsCmd { module: None, kind: HotspotKind::Outgoing, @@ -55,7 +55,7 @@ mod tests { } #[rstest] - fn test_hotspots_total(populated_db: cozo::DbInstance) { + fn test_hotspots_total(populated_db: db::DbInstance) { let cmd = HotspotsCmd { module: None, kind: HotspotKind::Total, @@ -73,7 +73,7 @@ mod tests { } #[rstest] - fn test_hotspots_ratio(populated_db: cozo::DbInstance) { + fn test_hotspots_ratio(populated_db: db::DbInstance) { let cmd = HotspotsCmd { module: None, kind: HotspotKind::Ratio, @@ -97,7 +97,7 @@ mod tests { // ========================================================================= #[rstest] - fn test_hotspots_with_module_filter(populated_db: cozo::DbInstance) { + fn test_hotspots_with_module_filter(populated_db: db::DbInstance) { let cmd = HotspotsCmd { module: Some("Accounts".to_string()), kind: HotspotKind::Incoming, @@ -115,7 +115,7 @@ mod tests { } #[rstest] - fn test_hotspots_with_limit(populated_db: cozo::DbInstance) { + fn test_hotspots_with_limit(populated_db: db::DbInstance) { let cmd = HotspotsCmd { module: None, kind: HotspotKind::Incoming, @@ -132,7 +132,7 @@ mod tests { } #[rstest] - fn test_hotspots_exclude_generated(populated_db: cozo::DbInstance) { + fn test_hotspots_exclude_generated(populated_db: db::DbInstance) { let cmd = HotspotsCmd { module: None, kind: HotspotKind::Incoming, diff --git a/src/commands/hotspots/mod.rs b/cli/src/commands/hotspots/mod.rs similarity index 96% rename from src/commands/hotspots/mod.rs rename to cli/src/commands/hotspots/mod.rs index 7dc4ad7..19d095c 100644 --- a/src/commands/hotspots/mod.rs +++ b/cli/src/commands/hotspots/mod.rs @@ -7,11 +7,11 @@ mod output_tests; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, CommonArgs, Execute}; use crate::output::{OutputFormat, Outputable}; -use crate::queries::hotspots::HotspotKind; +use db::queries::hotspots::HotspotKind; /// Find functions with the most incoming/outgoing calls #[derive(Args, Debug)] diff --git a/src/commands/hotspots/output.rs b/cli/src/commands/hotspots/output.rs similarity index 100% rename from src/commands/hotspots/output.rs rename to cli/src/commands/hotspots/output.rs diff --git a/src/commands/hotspots/output_tests.rs b/cli/src/commands/hotspots/output_tests.rs similarity index 100% rename from src/commands/hotspots/output_tests.rs rename to cli/src/commands/hotspots/output_tests.rs diff --git a/src/commands/import/cli_tests.rs b/cli/src/commands/import/cli_tests.rs similarity index 100% rename from src/commands/import/cli_tests.rs rename to cli/src/commands/import/cli_tests.rs diff --git a/src/commands/import/execute.rs b/cli/src/commands/import/execute.rs similarity index 97% rename from src/commands/import/execute.rs rename to cli/src/commands/import/execute.rs index 2bfe0dd..c4794c7 100644 --- a/src/commands/import/execute.rs +++ b/cli/src/commands/import/execute.rs @@ -1,12 +1,12 @@ use std::error::Error; use std::fs; -use cozo::DbInstance; +use db::DbInstance; use super::ImportCmd; use crate::commands::Execute; -use crate::queries::import::{clear_project_data, import_graph, ImportError, ImportResult}; -use crate::queries::import_models::CallGraph; +use db::queries::import::{clear_project_data, import_graph, ImportError, ImportResult}; +use db::queries::import_models::CallGraph; impl Execute for ImportCmd { type Output = ImportResult; @@ -39,7 +39,7 @@ impl Execute for ImportCmd { #[cfg(test)] mod tests { use super::*; - use crate::db::open_db; + use db::open_db; use rstest::{fixture, rstest}; use std::io::Write; use tempfile::NamedTempFile; diff --git a/src/commands/import/mod.rs b/cli/src/commands/import/mod.rs similarity index 98% rename from src/commands/import/mod.rs rename to cli/src/commands/import/mod.rs index 75d43c6..df7209a 100644 --- a/src/commands/import/mod.rs +++ b/cli/src/commands/import/mod.rs @@ -7,7 +7,7 @@ use std::error::Error; use std::path::PathBuf; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/import/output.rs b/cli/src/commands/import/output.rs similarity index 96% rename from src/commands/import/output.rs rename to cli/src/commands/import/output.rs index 1ab6ed4..4789965 100644 --- a/src/commands/import/output.rs +++ b/cli/src/commands/import/output.rs @@ -1,7 +1,7 @@ //! Output formatting for import command results. use crate::output::Outputable; -use crate::queries::import::ImportResult; +use db::queries::import::ImportResult; impl Outputable for ImportResult { fn to_table(&self) -> String { diff --git a/src/commands/import/output_tests.rs b/cli/src/commands/import/output_tests.rs similarity index 91% rename from src/commands/import/output_tests.rs rename to cli/src/commands/import/output_tests.rs index f40f0db..38ed595 100644 --- a/src/commands/import/output_tests.rs +++ b/cli/src/commands/import/output_tests.rs @@ -3,7 +3,7 @@ #[cfg(test)] mod tests { use crate::output::OutputFormat; - use crate::queries::import::{ImportResult, SchemaResult}; + use db::queries::import::{ImportResult, SchemaResult}; use rstest::{fixture, rstest}; const EMPTY_TABLE_OUTPUT: &str = "\ @@ -99,7 +99,7 @@ Created Schemas: test_name: test_format_json, fixture: full_result, fixture_type: ImportResult, - expected: crate::test_utils::load_output_fixture("import", "full.json"), + expected: db::test_utils::load_output_fixture("import", "full.json"), format: Json, } @@ -107,7 +107,7 @@ Created Schemas: test_name: test_format_toon, fixture: full_result, fixture_type: ImportResult, - expected: crate::test_utils::load_output_fixture("import", "full.toon"), + expected: db::test_utils::load_output_fixture("import", "full.toon"), format: Toon, } diff --git a/src/commands/large_functions/execute.rs b/cli/src/commands/large_functions/execute.rs similarity index 93% rename from src/commands/large_functions/execute.rs rename to cli/src/commands/large_functions/execute.rs index 576fcfd..088e08a 100644 --- a/src/commands/large_functions/execute.rs +++ b/cli/src/commands/large_functions/execute.rs @@ -5,8 +5,8 @@ use serde::Serialize; use super::LargeFunctionsCmd; use crate::commands::Execute; -use crate::queries::large_functions::find_large_functions; -use crate::types::{ModuleCollectionResult, ModuleGroup}; +use db::queries::large_functions::find_large_functions; +use db::types::{ModuleCollectionResult, ModuleGroup}; /// A single large function entry #[derive(Debug, Clone, Serialize)] @@ -22,7 +22,7 @@ pub struct LargeFunctionEntry { impl Execute for LargeFunctionsCmd { type Output = ModuleCollectionResult; - fn execute(self, db: &cozo::DbInstance) -> Result> { + fn execute(self, db: &db::DbInstance) -> Result> { let large_functions = find_large_functions( db, self.min_lines, diff --git a/src/commands/large_functions/mod.rs b/cli/src/commands/large_functions/mod.rs similarity index 98% rename from src/commands/large_functions/mod.rs rename to cli/src/commands/large_functions/mod.rs index 245fead..fdd70b2 100644 --- a/src/commands/large_functions/mod.rs +++ b/cli/src/commands/large_functions/mod.rs @@ -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}; diff --git a/src/commands/large_functions/output.rs b/cli/src/commands/large_functions/output.rs similarity index 96% rename from src/commands/large_functions/output.rs rename to cli/src/commands/large_functions/output.rs index f133de2..838930a 100644 --- a/src/commands/large_functions/output.rs +++ b/cli/src/commands/large_functions/output.rs @@ -2,7 +2,7 @@ use super::execute::LargeFunctionEntry; use crate::output::TableFormatter; -use crate::types::ModuleCollectionResult; +use db::types::ModuleCollectionResult; impl TableFormatter for ModuleCollectionResult { type Entry = LargeFunctionEntry; diff --git a/src/commands/location/cli_tests.rs b/cli/src/commands/location/cli_tests.rs similarity index 100% rename from src/commands/location/cli_tests.rs rename to cli/src/commands/location/cli_tests.rs diff --git a/src/commands/location/execute.rs b/cli/src/commands/location/execute.rs similarity index 95% rename from src/commands/location/execute.rs rename to cli/src/commands/location/execute.rs index fd1c6d6..8fed344 100644 --- a/src/commands/location/execute.rs +++ b/cli/src/commands/location/execute.rs @@ -5,7 +5,7 @@ use serde::Serialize; use super::LocationCmd; use crate::commands::Execute; -use crate::queries::location::{find_locations, FunctionLocation}; +use db::queries::location::{find_locations, FunctionLocation}; /// A single clause (definition) of a function #[derive(Debug, Clone, Serialize)] @@ -111,7 +111,7 @@ impl LocationResult { impl Execute for LocationCmd { type Output = LocationResult; - fn execute(self, db: &cozo::DbInstance) -> Result> { + fn execute(self, db: &db::DbInstance) -> Result> { let locations = find_locations( db, self.module.as_deref(), diff --git a/src/commands/location/execute_tests.rs b/cli/src/commands/location/execute_tests.rs similarity index 100% rename from src/commands/location/execute_tests.rs rename to cli/src/commands/location/execute_tests.rs diff --git a/src/commands/location/mod.rs b/cli/src/commands/location/mod.rs similarity index 98% rename from src/commands/location/mod.rs rename to cli/src/commands/location/mod.rs index b4b016e..49cb323 100644 --- a/src/commands/location/mod.rs +++ b/cli/src/commands/location/mod.rs @@ -7,7 +7,7 @@ mod output_tests; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, CommonArgs, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/location/output.rs b/cli/src/commands/location/output.rs similarity index 100% rename from src/commands/location/output.rs rename to cli/src/commands/location/output.rs diff --git a/src/commands/location/output_tests.rs b/cli/src/commands/location/output_tests.rs similarity index 95% rename from src/commands/location/output_tests.rs rename to cli/src/commands/location/output_tests.rs index 2c7f752..e0c5d48 100644 --- a/src/commands/location/output_tests.rs +++ b/cli/src/commands/location/output_tests.rs @@ -147,7 +147,7 @@ MyApp.Users: test_name: test_format_json, fixture: single_result, fixture_type: LocationResult, - expected: crate::test_utils::load_output_fixture("location", "single.json"), + expected: db::test_utils::load_output_fixture("location", "single.json"), format: Json, } @@ -155,7 +155,7 @@ MyApp.Users: test_name: test_format_toon, fixture: single_result, fixture_type: LocationResult, - expected: crate::test_utils::load_output_fixture("location", "single.toon"), + expected: db::test_utils::load_output_fixture("location", "single.toon"), format: Toon, } @@ -163,7 +163,7 @@ MyApp.Users: test_name: test_format_toon_empty, fixture: empty_result, fixture_type: LocationResult, - expected: crate::test_utils::load_output_fixture("location", "empty.toon"), + expected: db::test_utils::load_output_fixture("location", "empty.toon"), format: Toon, } } diff --git a/src/commands/many_clauses/execute.rs b/cli/src/commands/many_clauses/execute.rs similarity index 93% rename from src/commands/many_clauses/execute.rs rename to cli/src/commands/many_clauses/execute.rs index 32d8757..f8003a4 100644 --- a/src/commands/many_clauses/execute.rs +++ b/cli/src/commands/many_clauses/execute.rs @@ -5,8 +5,8 @@ use serde::Serialize; use super::ManyClausesCmd; use crate::commands::Execute; -use crate::queries::many_clauses::find_many_clauses; -use crate::types::{ModuleCollectionResult, ModuleGroup}; +use db::queries::many_clauses::find_many_clauses; +use db::types::{ModuleCollectionResult, ModuleGroup}; /// A single function with many clauses entry #[derive(Debug, Clone, Serialize)] @@ -22,7 +22,7 @@ pub struct ManyClausesEntry { impl Execute for ManyClausesCmd { type Output = ModuleCollectionResult; - fn execute(self, db: &cozo::DbInstance) -> Result> { + fn execute(self, db: &db::DbInstance) -> Result> { let many_clauses = find_many_clauses( db, self.min_clauses, diff --git a/src/commands/many_clauses/mod.rs b/cli/src/commands/many_clauses/mod.rs similarity index 98% rename from src/commands/many_clauses/mod.rs rename to cli/src/commands/many_clauses/mod.rs index 3195bc2..d812501 100644 --- a/src/commands/many_clauses/mod.rs +++ b/cli/src/commands/many_clauses/mod.rs @@ -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}; diff --git a/src/commands/many_clauses/output.rs b/cli/src/commands/many_clauses/output.rs similarity index 96% rename from src/commands/many_clauses/output.rs rename to cli/src/commands/many_clauses/output.rs index 32b9a33..40831a4 100644 --- a/src/commands/many_clauses/output.rs +++ b/cli/src/commands/many_clauses/output.rs @@ -2,7 +2,7 @@ use super::execute::ManyClausesEntry; use crate::output::TableFormatter; -use crate::types::ModuleCollectionResult; +use db::types::ModuleCollectionResult; impl TableFormatter for ModuleCollectionResult { type Entry = ManyClausesEntry; diff --git a/src/commands/mod.rs b/cli/src/commands/mod.rs similarity index 98% rename from src/commands/mod.rs rename to cli/src/commands/mod.rs index eac19b7..d17418b 100644 --- a/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -91,7 +91,7 @@ use clap::Subcommand; use enum_dispatch::enum_dispatch; use std::error::Error; -use cozo::DbInstance; +use db::DbInstance; use crate::output::{OutputFormat, Outputable}; @@ -99,7 +99,7 @@ use crate::output::{OutputFormat, Outputable}; pub trait Execute { type Output: Outputable; - fn execute(self, db: &DbInstance) -> Result>; + fn execute(self, db: &db::DbInstance) -> Result>; } /// Trait for commands that can be executed and formatted. diff --git a/src/commands/path/cli_tests.rs b/cli/src/commands/path/cli_tests.rs similarity index 100% rename from src/commands/path/cli_tests.rs rename to cli/src/commands/path/cli_tests.rs diff --git a/src/commands/path/execute.rs b/cli/src/commands/path/execute.rs similarity index 88% rename from src/commands/path/execute.rs rename to cli/src/commands/path/execute.rs index 2dee335..100cdbf 100644 --- a/src/commands/path/execute.rs +++ b/cli/src/commands/path/execute.rs @@ -4,7 +4,7 @@ use serde::Serialize; use super::PathCmd; use crate::commands::Execute; -use crate::queries::path::{find_paths, CallPath}; +use db::queries::path::{find_paths, CallPath}; /// Result of the path command execution #[derive(Debug, Default, Serialize)] @@ -20,7 +20,7 @@ pub struct PathResult { impl Execute for PathCmd { type Output = PathResult; - fn execute(self, db: &cozo::DbInstance) -> Result> { + fn execute(self, db: &db::DbInstance) -> Result> { let mut result = PathResult { from_module: self.from_module.clone(), from_function: self.from_function.clone(), diff --git a/src/commands/path/execute_tests.rs b/cli/src/commands/path/execute_tests.rs similarity index 100% rename from src/commands/path/execute_tests.rs rename to cli/src/commands/path/execute_tests.rs diff --git a/src/commands/path/mod.rs b/cli/src/commands/path/mod.rs similarity index 98% rename from src/commands/path/mod.rs rename to cli/src/commands/path/mod.rs index 75d4f25..67ab3c7 100644 --- a/src/commands/path/mod.rs +++ b/cli/src/commands/path/mod.rs @@ -7,7 +7,7 @@ mod output_tests; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/path/output.rs b/cli/src/commands/path/output.rs similarity index 100% rename from src/commands/path/output.rs rename to cli/src/commands/path/output.rs diff --git a/src/commands/path/output_tests.rs b/cli/src/commands/path/output_tests.rs similarity index 92% rename from src/commands/path/output_tests.rs rename to cli/src/commands/path/output_tests.rs index 1fe1805..c520666 100644 --- a/src/commands/path/output_tests.rs +++ b/cli/src/commands/path/output_tests.rs @@ -3,7 +3,7 @@ #[cfg(test)] mod tests { use super::super::execute::PathResult; - use crate::queries::path::{CallPath, PathStep}; + use db::queries::path::{CallPath, PathStep}; use rstest::{fixture, rstest}; // ========================================================================= @@ -100,7 +100,7 @@ Path 1: test_name: test_format_json, fixture: single_path_result, fixture_type: PathResult, - expected: crate::test_utils::load_output_fixture("path", "single.json"), + expected: db::test_utils::load_output_fixture("path", "single.json"), format: Json, } @@ -108,7 +108,7 @@ Path 1: test_name: test_format_toon, fixture: single_path_result, fixture_type: PathResult, - expected: crate::test_utils::load_output_fixture("path", "single.toon"), + expected: db::test_utils::load_output_fixture("path", "single.toon"), format: Toon, } @@ -116,7 +116,7 @@ Path 1: test_name: test_format_toon_empty, fixture: empty_result, fixture_type: PathResult, - expected: crate::test_utils::load_output_fixture("path", "empty.toon"), + expected: db::test_utils::load_output_fixture("path", "empty.toon"), format: Toon, } } diff --git a/cli/src/commands/returns/execute.rs b/cli/src/commands/returns/execute.rs new file mode 100644 index 0000000..0cfbe85 --- /dev/null +++ b/cli/src/commands/returns/execute.rs @@ -0,0 +1,65 @@ +use std::error::Error; + +use serde::Serialize; + +use super::ReturnsCmd; +use crate::commands::Execute; +use db::queries::returns::{find_returns, ReturnEntry}; +use db::types::ModuleGroupResult; + +/// A function's return type information +#[derive(Debug, Clone, Serialize)] +pub struct ReturnInfo { + pub name: String, + pub arity: i64, + pub return_type: String, + pub line: i64, +} + +/// Build grouped result from flat ReturnEntry list +fn build_return_info_result( + pattern: String, + module_filter: Option, + entries: Vec, +) -> ModuleGroupResult { + let total_items = entries.len(); + + // Use helper to group by module + let items = crate::utils::group_by_module(entries, |entry| { + let return_info = ReturnInfo { + name: entry.name, + arity: entry.arity, + return_type: entry.return_string, + line: entry.line, + }; + (entry.module, return_info) + }); + + ModuleGroupResult { + module_pattern: module_filter.unwrap_or_else(|| "*".to_string()), + function_pattern: Some(pattern), + total_items, + items, + } +} + +impl Execute for ReturnsCmd { + type Output = ModuleGroupResult; + + fn execute(self, db: &db::DbInstance) -> Result> { + let entries = find_returns( + db, + &self.pattern, + &self.common.project, + self.common.regex, + self.module.as_deref(), + self.common.limit, + )?; + + Ok(build_return_info_result( + self.pattern, + self.module, + entries, + )) + } +} diff --git a/src/commands/returns/mod.rs b/cli/src/commands/returns/mod.rs similarity index 97% rename from src/commands/returns/mod.rs rename to cli/src/commands/returns/mod.rs index 98d7674..5f9c431 100644 --- a/src/commands/returns/mod.rs +++ b/cli/src/commands/returns/mod.rs @@ -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}; diff --git a/src/commands/returns/output.rs b/cli/src/commands/returns/output.rs similarity index 87% rename from src/commands/returns/output.rs rename to cli/src/commands/returns/output.rs index f4c904d..b206683 100644 --- a/src/commands/returns/output.rs +++ b/cli/src/commands/returns/output.rs @@ -1,14 +1,14 @@ //! Output formatting for returns command results. use crate::output::TableFormatter; -use crate::types::ModuleGroupResult; +use db::types::ModuleGroupResult; use super::execute::ReturnInfo; impl TableFormatter for ModuleGroupResult { type Entry = ReturnInfo; 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 returning \"{}\"", pattern) } diff --git a/src/commands/reverse_trace/cli_tests.rs b/cli/src/commands/reverse_trace/cli_tests.rs similarity index 100% rename from src/commands/reverse_trace/cli_tests.rs rename to cli/src/commands/reverse_trace/cli_tests.rs diff --git a/cli/src/commands/reverse_trace/execute.rs b/cli/src/commands/reverse_trace/execute.rs new file mode 100644 index 0000000..aa96d56 --- /dev/null +++ b/cli/src/commands/reverse_trace/execute.rs @@ -0,0 +1,155 @@ +use std::collections::HashMap; +use std::error::Error; + +use super::ReverseTraceCmd; +use crate::commands::Execute; +use db::queries::reverse_trace::{reverse_trace_calls, ReverseTraceStep}; +use db::types::{TraceDirection, TraceEntry, TraceResult}; + +/// Build a flattened reverse-trace from ReverseTraceStep objects +fn build_reverse_trace_result( + target_module: String, + target_function: String, + max_depth: u32, + steps: Vec, +) -> TraceResult { + let mut entries = Vec::new(); + let mut entry_index_map: HashMap<(String, String, i64, i64), usize> = HashMap::new(); + + if steps.is_empty() { + return TraceResult::empty(target_module, target_function, max_depth, TraceDirection::Backward); + } + + // Group steps by depth + let mut by_depth: HashMap> = HashMap::new(); + for step in &steps { + by_depth.entry(step.depth).or_default().push(step); + } + + // Process depth 1 (direct callers of target function) + if let Some(depth1_steps) = by_depth.get(&1) { + let mut filter = crate::dedup::DeduplicationFilter::new(); + + for step in depth1_steps { + let caller_key = ( + step.caller_module.clone(), + step.caller_function.clone(), + step.caller_arity, + 1i64, + ); + + // Add caller as root entry if not already added + if filter.should_process(caller_key.clone()) { + let entry_idx = entries.len(); + entries.push(TraceEntry { + module: step.caller_module.clone(), + function: step.caller_function.clone(), + arity: step.caller_arity, + kind: step.caller_kind.clone(), + start_line: step.caller_start_line, + end_line: step.caller_end_line, + file: step.file.clone(), + depth: 1, + line: step.line, + parent_index: None, + }); + entry_index_map.insert(caller_key, entry_idx); + } + } + } + + // Process deeper levels (additional callers) + for depth in 2..=max_depth as i64 { + if let Some(depth_steps) = by_depth.get(&depth) { + let mut filter = crate::dedup::DeduplicationFilter::new(); + + for step in depth_steps { + let caller_key = ( + step.caller_module.clone(), + step.caller_function.clone(), + step.caller_arity, + depth, + ); + + // Find parent index (the callee at previous depth, which is what called this caller) + let parent_key = ( + step.callee_module.clone(), + step.callee_function.clone(), + step.callee_arity, + depth - 1, + ); + + let parent_index = entry_index_map.get(&parent_key).copied(); + + if filter.should_process(caller_key.clone()) && parent_index.is_some() { + let entry_idx = entries.len(); + entries.push(TraceEntry { + module: step.caller_module.clone(), + function: step.caller_function.clone(), + arity: step.caller_arity, + kind: step.caller_kind.clone(), + start_line: step.caller_start_line, + end_line: step.caller_end_line, + file: step.file.clone(), + depth, + line: step.line, + parent_index, + }); + entry_index_map.insert(caller_key, entry_idx); + } + } + } + } + + let total_items = entries.len(); + + TraceResult { + module: target_module, + function: target_function, + max_depth, + direction: TraceDirection::Backward, + total_items, + entries, + } +} + +impl Execute for ReverseTraceCmd { + type Output = TraceResult; + + fn execute(self, db: &db::DbInstance) -> Result> { + let steps = reverse_trace_calls( + db, + &self.module, + &self.function, + self.arity, + &self.common.project, + self.common.regex, + self.depth, + self.common.limit, + )?; + + Ok(build_reverse_trace_result( + self.module, + self.function, + self.depth, + steps, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_empty_reverse_trace() { + let result = build_reverse_trace_result( + "TestModule".to_string(), + "test_func".to_string(), + 5, + vec![], + ); + assert_eq!(result.total_items, 0); + assert_eq!(result.entries.len(), 0); + } +} diff --git a/src/commands/reverse_trace/execute_tests.rs b/cli/src/commands/reverse_trace/execute_tests.rs similarity index 100% rename from src/commands/reverse_trace/execute_tests.rs rename to cli/src/commands/reverse_trace/execute_tests.rs diff --git a/src/commands/reverse_trace/mod.rs b/cli/src/commands/reverse_trace/mod.rs similarity index 98% rename from src/commands/reverse_trace/mod.rs rename to cli/src/commands/reverse_trace/mod.rs index 9b974b9..092523b 100644 --- a/src/commands/reverse_trace/mod.rs +++ b/cli/src/commands/reverse_trace/mod.rs @@ -7,7 +7,7 @@ mod output_tests; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, CommonArgs, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/reverse_trace/output.rs b/cli/src/commands/reverse_trace/output.rs similarity index 100% rename from src/commands/reverse_trace/output.rs rename to cli/src/commands/reverse_trace/output.rs diff --git a/src/commands/reverse_trace/output_tests.rs b/cli/src/commands/reverse_trace/output_tests.rs similarity index 98% rename from src/commands/reverse_trace/output_tests.rs rename to cli/src/commands/reverse_trace/output_tests.rs index ab4cbe5..8aa0baa 100644 --- a/src/commands/reverse_trace/output_tests.rs +++ b/cli/src/commands/reverse_trace/output_tests.rs @@ -2,7 +2,7 @@ #[cfg(test)] mod tests { - use crate::types::{TraceDirection, TraceEntry, TraceResult}; + use db::types::{TraceDirection, TraceEntry, TraceResult}; use rstest::{fixture, rstest}; // ========================================================================= diff --git a/src/commands/search/cli_tests.rs b/cli/src/commands/search/cli_tests.rs similarity index 100% rename from src/commands/search/cli_tests.rs rename to cli/src/commands/search/cli_tests.rs diff --git a/src/commands/search/execute.rs b/cli/src/commands/search/execute.rs similarity index 93% rename from src/commands/search/execute.rs rename to cli/src/commands/search/execute.rs index 410a224..065eeef 100644 --- a/src/commands/search/execute.rs +++ b/cli/src/commands/search/execute.rs @@ -5,7 +5,7 @@ use serde::Serialize; use super::{SearchCmd, SearchKind}; use crate::commands::Execute; -use crate::queries::search::{search_functions, search_modules, FunctionResult as RawFunctionResult, ModuleResult}; +use db::queries::search::{search_functions, search_modules, FunctionResult as RawFunctionResult, ModuleResult}; /// A function found in search results #[derive(Debug, Clone, Serialize)] @@ -72,7 +72,7 @@ impl SearchResult { impl Execute for SearchCmd { type Output = SearchResult; - fn execute(self, db: &cozo::DbInstance) -> Result> { + fn execute(self, db: &db::DbInstance) -> Result> { match self.kind { SearchKind::Modules => { let modules = search_modules(db, &self.pattern, &self.common.project, self.common.limit, self.common.regex)?; diff --git a/src/commands/search/execute_tests.rs b/cli/src/commands/search/execute_tests.rs similarity index 100% rename from src/commands/search/execute_tests.rs rename to cli/src/commands/search/execute_tests.rs diff --git a/src/commands/search/mod.rs b/cli/src/commands/search/mod.rs similarity index 98% rename from src/commands/search/mod.rs rename to cli/src/commands/search/mod.rs index 3b4c2fa..828eb7e 100644 --- a/src/commands/search/mod.rs +++ b/cli/src/commands/search/mod.rs @@ -7,7 +7,7 @@ mod output_tests; use std::error::Error; use clap::{Args, ValueEnum}; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, CommonArgs, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/search/output.rs b/cli/src/commands/search/output.rs similarity index 100% rename from src/commands/search/output.rs rename to cli/src/commands/search/output.rs diff --git a/src/commands/search/output_tests.rs b/cli/src/commands/search/output_tests.rs similarity index 92% rename from src/commands/search/output_tests.rs rename to cli/src/commands/search/output_tests.rs index 8a5785a..d895891 100644 --- a/src/commands/search/output_tests.rs +++ b/cli/src/commands/search/output_tests.rs @@ -3,7 +3,7 @@ #[cfg(test)] mod tests { use super::super::execute::{SearchFunc, SearchFuncModule, SearchResult}; - use crate::queries::search::ModuleResult; + use db::queries::search::ModuleResult; use rstest::{fixture, rstest}; // ========================================================================= @@ -115,7 +115,7 @@ MyApp.Accounts: test_name: test_format_json, fixture: modules_result, fixture_type: SearchResult, - expected: crate::test_utils::load_output_fixture("search", "modules.json"), + expected: db::test_utils::load_output_fixture("search", "modules.json"), format: Json, } @@ -123,7 +123,7 @@ MyApp.Accounts: test_name: test_format_toon, fixture: modules_result, fixture_type: SearchResult, - expected: crate::test_utils::load_output_fixture("search", "modules.toon"), + expected: db::test_utils::load_output_fixture("search", "modules.toon"), format: Toon, } @@ -131,7 +131,7 @@ MyApp.Accounts: test_name: test_format_toon_empty, fixture: empty_result, fixture_type: SearchResult, - expected: crate::test_utils::load_output_fixture("search", "empty.toon"), + expected: db::test_utils::load_output_fixture("search", "empty.toon"), format: Toon, } } diff --git a/src/commands/setup/execute.rs b/cli/src/commands/setup/execute.rs similarity index 99% rename from src/commands/setup/execute.rs rename to cli/src/commands/setup/execute.rs index ee80a88..b98597e 100644 --- a/src/commands/setup/execute.rs +++ b/cli/src/commands/setup/execute.rs @@ -1,21 +1,21 @@ use std::error::Error; use std::fs; -use cozo::DbInstance; +use db::DbInstance; use include_dir::{include_dir, Dir}; use serde::Serialize; use super::SetupCmd; use crate::commands::Execute; -use crate::queries::schema; +use db::queries::schema; /// Embedded skill templates directory -static SKILL_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/skills"); +static SKILL_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/../templates/skills"); /// Embedded agent templates directory -static AGENT_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/agents"); +static AGENT_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/../templates/agents"); /// Embedded hook templates directory -static HOOK_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/hooks"); +static HOOK_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/../templates/hooks"); /// Status of a database relation (table) #[derive(Debug, Clone, Serialize)] @@ -382,7 +382,7 @@ impl Execute for SetupCmd { #[cfg(test)] mod tests { use super::*; - use crate::db::open_db; + use db::open_db; use rstest::{fixture, rstest}; use tempfile::NamedTempFile; diff --git a/src/commands/setup/mod.rs b/cli/src/commands/setup/mod.rs similarity index 98% rename from src/commands/setup/mod.rs rename to cli/src/commands/setup/mod.rs index 463dac9..2892646 100644 --- a/src/commands/setup/mod.rs +++ b/cli/src/commands/setup/mod.rs @@ -3,7 +3,7 @@ mod output; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/setup/output.rs b/cli/src/commands/setup/output.rs similarity index 100% rename from src/commands/setup/output.rs rename to cli/src/commands/setup/output.rs diff --git a/src/commands/struct_usage/cli_tests.rs b/cli/src/commands/struct_usage/cli_tests.rs similarity index 100% rename from src/commands/struct_usage/cli_tests.rs rename to cli/src/commands/struct_usage/cli_tests.rs diff --git a/cli/src/commands/struct_usage/execute.rs b/cli/src/commands/struct_usage/execute.rs new file mode 100644 index 0000000..4769265 --- /dev/null +++ b/cli/src/commands/struct_usage/execute.rs @@ -0,0 +1,173 @@ +use std::collections::{BTreeMap, HashSet}; +use std::error::Error; + +use serde::Serialize; + +use super::StructUsageCmd; +use crate::commands::Execute; +use db::queries::struct_usage::{find_struct_usage, StructUsageEntry}; +use db::types::ModuleGroupResult; + +/// A function that uses a struct type +#[derive(Debug, Clone, Serialize)] +pub struct UsageInfo { + pub name: String, + pub arity: i64, + pub inputs: String, + pub returns: String, + pub line: i64, +} + +/// A module and its usage counts for a struct type +#[derive(Debug, Clone, Serialize)] +pub struct ModuleStructUsage { + pub name: String, + pub accepts_count: i64, + pub returns_count: i64, + pub total: i64, +} + +/// Result containing aggregated module-level struct usage +#[derive(Debug, Clone, Serialize)] +pub struct StructModulesResult { + pub struct_pattern: String, + pub total_modules: usize, + pub total_functions: usize, + pub modules: Vec, +} + +/// Output type that can be either detailed or aggregated +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum StructUsageOutput { + Detailed(ModuleGroupResult), + ByModule(StructModulesResult), +} + +/// Build grouped result from flat StructUsageEntry list +fn build_usage_info_result( + pattern: String, + module_filter: Option, + entries: Vec, +) -> ModuleGroupResult { + let total_items = entries.len(); + + // Use helper to group by module + let items = crate::utils::group_by_module(entries, |entry| { + let usage_info = UsageInfo { + name: entry.name, + arity: entry.arity, + inputs: entry.inputs_string, + returns: entry.return_string, + line: entry.line, + }; + (entry.module, usage_info) + }); + + ModuleGroupResult { + module_pattern: module_filter.unwrap_or_else(|| "*".to_string()), + function_pattern: Some(pattern), + total_items, + items, + } +} + +/// Build aggregated result from flat StructUsageEntry list +fn build_struct_modules_result(pattern: String, entries: Vec) -> StructModulesResult { + // Aggregate by module, tracking which functions accept vs return + let mut module_map: BTreeMap> = BTreeMap::new(); + let mut module_accepts: BTreeMap> = BTreeMap::new(); + let mut module_returns: BTreeMap> = BTreeMap::new(); + + for entry in &entries { + // Track unique functions per module + module_map + .entry(entry.module.clone()) + .or_default() + .insert(format!("{}/{}", entry.name, entry.arity)); + + // Check if function accepts the type + if entry.inputs_string.contains(&pattern) { + module_accepts + .entry(entry.module.clone()) + .or_default() + .insert(format!("{}/{}", entry.name, entry.arity)); + } + + // Check if function returns the type + if entry.return_string.contains(&pattern) { + module_returns + .entry(entry.module.clone()) + .or_default() + .insert(format!("{}/{}", entry.name, entry.arity)); + } + } + + // Convert to result type, sorted by total count descending + let mut modules: Vec = module_map + .into_iter() + .map(|(name, functions)| { + let accepts_count = module_accepts + .get(&name) + .map(|s| s.len() as i64) + .unwrap_or(0); + let returns_count = module_returns + .get(&name) + .map(|s| s.len() as i64) + .unwrap_or(0); + let total = functions.len() as i64; + + ModuleStructUsage { + name, + accepts_count, + returns_count, + total, + } + }) + .collect(); + + // Sort by total count descending, then by module name + modules.sort_by(|a, b| { + let cmp = b.total.cmp(&a.total); + if cmp == std::cmp::Ordering::Equal { + a.name.cmp(&b.name) + } else { + cmp + } + }); + + let total_modules = modules.len(); + let total_functions = entries.len(); + + StructModulesResult { + struct_pattern: pattern, + total_modules, + total_functions, + modules, + } +} + +impl Execute for StructUsageCmd { + type Output = StructUsageOutput; + + fn execute(self, db: &db::DbInstance) -> Result> { + let entries = find_struct_usage( + db, + &self.pattern, + &self.common.project, + self.common.regex, + self.module.as_deref(), + self.common.limit, + )?; + + if self.by_module { + Ok(StructUsageOutput::ByModule( + build_struct_modules_result(self.pattern, entries), + )) + } else { + Ok(StructUsageOutput::Detailed( + build_usage_info_result(self.pattern, self.module, entries), + )) + } + } +} diff --git a/src/commands/struct_usage/execute_tests.rs b/cli/src/commands/struct_usage/execute_tests.rs similarity index 100% rename from src/commands/struct_usage/execute_tests.rs rename to cli/src/commands/struct_usage/execute_tests.rs diff --git a/src/commands/struct_usage/mod.rs b/cli/src/commands/struct_usage/mod.rs similarity index 98% rename from src/commands/struct_usage/mod.rs rename to cli/src/commands/struct_usage/mod.rs index a1c4f13..be61e19 100644 --- a/src/commands/struct_usage/mod.rs +++ b/cli/src/commands/struct_usage/mod.rs @@ -11,7 +11,7 @@ mod output_tests; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, CommonArgs, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/struct_usage/output.rs b/cli/src/commands/struct_usage/output.rs similarity index 97% rename from src/commands/struct_usage/output.rs rename to cli/src/commands/struct_usage/output.rs index a430ee2..c5fb9ab 100644 --- a/src/commands/struct_usage/output.rs +++ b/cli/src/commands/struct_usage/output.rs @@ -4,7 +4,7 @@ use regex::Regex; use std::sync::LazyLock; use crate::output::{Outputable, TableFormatter}; -use crate::types::ModuleGroupResult; +use db::types::ModuleGroupResult; use super::execute::{UsageInfo, StructUsageOutput, StructModulesResult}; /// Regex to match Elixir struct maps like `%{__struct__: Module.Name, field: type(), ...}` @@ -22,7 +22,7 @@ impl TableFormatter for ModuleGroupResult { type Entry = UsageInfo; 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 using \"{}\"", pattern) } diff --git a/src/commands/struct_usage/output_tests.rs b/cli/src/commands/struct_usage/output_tests.rs similarity index 99% rename from src/commands/struct_usage/output_tests.rs rename to cli/src/commands/struct_usage/output_tests.rs index 715ae09..b1ed160 100644 --- a/src/commands/struct_usage/output_tests.rs +++ b/cli/src/commands/struct_usage/output_tests.rs @@ -3,7 +3,7 @@ #[cfg(test)] mod tests { use super::super::execute::{ModuleStructUsage, StructModulesResult, StructUsageOutput, UsageInfo}; - use crate::types::{ModuleGroup, ModuleGroupResult}; + use db::types::{ModuleGroup, ModuleGroupResult}; use rstest::{fixture, rstest}; // ========================================================================= diff --git a/src/commands/trace/cli_tests.rs b/cli/src/commands/trace/cli_tests.rs similarity index 100% rename from src/commands/trace/cli_tests.rs rename to cli/src/commands/trace/cli_tests.rs diff --git a/cli/src/commands/trace/execute.rs b/cli/src/commands/trace/execute.rs new file mode 100644 index 0000000..a4a28d3 --- /dev/null +++ b/cli/src/commands/trace/execute.rs @@ -0,0 +1,175 @@ +use std::collections::HashMap; +use std::error::Error; + +use super::TraceCmd; +use crate::commands::Execute; +use db::queries::trace::trace_calls; +use db::types::{Call, TraceDirection, TraceEntry, TraceResult}; + +fn build_trace_result( + start_module: String, + start_function: String, + max_depth: u32, + calls: Vec, +) -> TraceResult { + let mut entries = Vec::new(); + let mut entry_index_map: HashMap<(String, String, i64, i64), usize> = HashMap::new(); + + // Add the starting function as the root entry at depth 0 + entries.push(TraceEntry { + module: start_module.clone(), + function: start_function.clone(), + arity: 0, // Will be updated from first call if available + kind: String::new(), + start_line: 0, + end_line: 0, + file: String::new(), + depth: 0, + line: 0, + parent_index: None, + }); + entry_index_map.insert((start_module.clone(), start_function.clone(), 0, 0), 0); + + if calls.is_empty() { + return TraceResult::empty(start_module, start_function, max_depth, TraceDirection::Forward); + } + + // Group calls by depth, consuming the Vec to take ownership + let mut by_depth: HashMap> = HashMap::new(); + for call in calls { + if let Some(depth) = call.depth { + by_depth.entry(depth).or_default().push(call); + } + } + + // Process depth 1 (direct callees from start function) + if let Some(depth1_calls) = by_depth.remove(&1) { + // Track seen entries by index into entries vec (avoids storing strings) + let mut seen_at_depth: std::collections::HashSet = std::collections::HashSet::new(); + + for call in depth1_calls { + // Check if we already have this callee at this depth + let existing = entries.iter().position(|e| { + e.depth == 1 + && e.module == call.callee.module.as_ref() + && e.function == call.callee.name.as_ref() + && e.arity == call.callee.arity + }); + + if (existing.is_none() || seen_at_depth.insert(existing.unwrap_or(usize::MAX))) + && existing.is_none() { + let entry_idx = entries.len(); + // Convert from Rc to String for storage + let module = call.callee.module.to_string(); + let function = call.callee.name.to_string(); + let arity = call.callee.arity; + entry_index_map.insert((module.clone(), function.clone(), arity, 1i64), entry_idx); + entries.push(TraceEntry { + module, + function, + arity, + kind: call.callee.kind.as_deref().unwrap_or("").to_string(), + start_line: call.callee.start_line.unwrap_or(0), + end_line: call.callee.end_line.unwrap_or(0), + file: call.callee.file.as_deref().unwrap_or("").to_string(), + depth: 1, + line: call.line, + parent_index: Some(0), + }); + } + } + } + + // Process deeper levels + for depth in 2..=max_depth as i64 { + if let Some(depth_calls) = by_depth.remove(&depth) { + for call in depth_calls { + // Check if we already have this callee at this depth + let existing = entries.iter().position(|e| { + e.depth == depth + && e.module == call.callee.module.as_ref() + && e.function == call.callee.name.as_ref() + && e.arity == call.callee.arity + }); + + if existing.is_none() { + // Find parent index using references (no cloning) + let parent_index = entries.iter().position(|e| { + e.depth == depth - 1 + && e.module == call.caller.module.as_ref() + && e.function == call.caller.name.as_ref() + && e.arity == call.caller.arity + }); + + if parent_index.is_some() { + let entry_idx = entries.len(); + // Convert from Rc to String for storage + let module = call.callee.module.to_string(); + let function = call.callee.name.to_string(); + let arity = call.callee.arity; + entry_index_map.insert((module.clone(), function.clone(), arity, depth), entry_idx); + entries.push(TraceEntry { + module, + function, + arity, + kind: call.callee.kind.as_deref().unwrap_or("").to_string(), + start_line: call.callee.start_line.unwrap_or(0), + end_line: call.callee.end_line.unwrap_or(0), + file: call.callee.file.as_deref().unwrap_or("").to_string(), + depth, + line: call.line, + parent_index, + }); + } + } + } + } + } + + let total_items = entries.len() - 1; // Exclude the root entry from count + + TraceResult { + module: start_module, + function: start_function, + max_depth, + direction: TraceDirection::Forward, + total_items, + entries, + } +} + +impl Execute for TraceCmd { + type Output = TraceResult; + + fn execute(self, db: &db::DbInstance) -> Result> { + let calls = trace_calls( + db, + &self.module, + &self.function, + self.arity, + &self.common.project, + self.common.regex, + self.depth, + self.common.limit, + )?; + + Ok(build_trace_result( + self.module, + self.function, + self.depth, + calls, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_empty_trace() { + let result = TraceResult::empty("TestModule".to_string(), "test_func".to_string(), 5, db::TraceDirection::Forward); + assert_eq!(result.total_items, 0); + assert_eq!(result.entries.len(), 0); + } +} diff --git a/src/commands/trace/execute_tests.rs b/cli/src/commands/trace/execute_tests.rs similarity index 100% rename from src/commands/trace/execute_tests.rs rename to cli/src/commands/trace/execute_tests.rs diff --git a/src/commands/trace/mod.rs b/cli/src/commands/trace/mod.rs similarity index 98% rename from src/commands/trace/mod.rs rename to cli/src/commands/trace/mod.rs index 06faf02..6ecc5ab 100644 --- a/src/commands/trace/mod.rs +++ b/cli/src/commands/trace/mod.rs @@ -7,7 +7,7 @@ mod output_tests; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, CommonArgs, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/trace/output.rs b/cli/src/commands/trace/output.rs similarity index 94% rename from src/commands/trace/output.rs rename to cli/src/commands/trace/output.rs index a09379d..494307b 100644 --- a/src/commands/trace/output.rs +++ b/cli/src/commands/trace/output.rs @@ -1,7 +1,7 @@ //! Output formatting for trace and reverse-trace command results. use crate::output::Outputable; -use crate::types::{TraceResult, TraceDirection}; +use db::types::{TraceResult, TraceDirection}; impl Outputable for TraceResult { fn to_table(&self) -> String { @@ -67,7 +67,7 @@ fn format_reverse_trace(result: &TraceResult) -> String { } /// Format a reverse trace entry (callers going up the chain) -fn format_reverse_entry(lines: &mut Vec, entries: &[crate::types::TraceEntry], idx: usize, depth: usize) { +fn format_reverse_entry(lines: &mut Vec, entries: &[db::types::TraceEntry], idx: usize, depth: usize) { let entry = &entries[idx]; let indent = " ".repeat(depth); let kind_str = if entry.kind.is_empty() { @@ -104,7 +104,7 @@ fn format_reverse_entry(lines: &mut Vec, entries: &[crate::types::TraceE } /// Recursively format an entry and its children -fn format_entry(lines: &mut Vec, entries: &[crate::types::TraceEntry], idx: usize, depth: usize) { +fn format_entry(lines: &mut Vec, entries: &[db::types::TraceEntry], idx: usize, depth: usize) { let entry = &entries[idx]; let indent = " ".repeat(depth); let kind_str = if entry.kind.is_empty() { @@ -133,7 +133,7 @@ fn format_entry(lines: &mut Vec, entries: &[crate::types::TraceEntry], i /// Format a child call/caller entry fn format_call( lines: &mut Vec, - entries: &[crate::types::TraceEntry], + entries: &[db::types::TraceEntry], idx: usize, depth: usize, parent_module: &str, diff --git a/src/commands/trace/output_tests.rs b/cli/src/commands/trace/output_tests.rs similarity index 98% rename from src/commands/trace/output_tests.rs rename to cli/src/commands/trace/output_tests.rs index d26d11c..6b04d0e 100644 --- a/src/commands/trace/output_tests.rs +++ b/cli/src/commands/trace/output_tests.rs @@ -2,7 +2,7 @@ #[cfg(test)] mod tests { - use crate::types::{TraceDirection, TraceEntry, TraceResult}; + use db::types::{TraceDirection, TraceEntry, TraceResult}; use rstest::{fixture, rstest}; // ========================================================================= diff --git a/src/commands/unused/cli_tests.rs b/cli/src/commands/unused/cli_tests.rs similarity index 100% rename from src/commands/unused/cli_tests.rs rename to cli/src/commands/unused/cli_tests.rs diff --git a/cli/src/commands/unused/execute.rs b/cli/src/commands/unused/execute.rs new file mode 100644 index 0000000..632613a --- /dev/null +++ b/cli/src/commands/unused/execute.rs @@ -0,0 +1,68 @@ +use std::error::Error; + +use serde::Serialize; + +use super::UnusedCmd; +use crate::commands::Execute; +use db::queries::unused::{find_unused_functions, UnusedFunction}; +use db::types::ModuleCollectionResult; + +/// An unused function within a module +#[derive(Debug, Clone, Serialize)] +pub struct UnusedFunc { + pub name: String, + pub arity: i64, + pub kind: String, + pub line: i64, +} + +/// Build grouped result from flat UnusedFunction list +fn build_unused_functions_result( + module_pattern: String, + functions: Vec, +) -> ModuleCollectionResult { + let total_items = functions.len(); + + // Use helper to group by module, tracking file for each module + let items = crate::utils::group_by_module_with_file(functions, |func| { + let unused_func = UnusedFunc { + name: func.name, + arity: func.arity, + kind: func.kind, + line: func.line, + }; + (func.module, unused_func, func.file) + }); + + ModuleCollectionResult { + module_pattern, + function_pattern: None, + kind_filter: None, + name_filter: None, + total_items, + items, + } +} + +impl Execute for UnusedCmd { + type Output = ModuleCollectionResult; + + fn execute(self, db: &db::DbInstance) -> Result> { + let functions = find_unused_functions( + db, + self.module.as_deref(), + &self.common.project, + self.common.regex, + self.private_only, + self.public_only, + self.exclude_generated, + self.common.limit, + )?; + + Ok(build_unused_functions_result( + self.module.unwrap_or_else(|| "*".to_string()), + functions, + )) + } +} + diff --git a/src/commands/unused/execute_tests.rs b/cli/src/commands/unused/execute_tests.rs similarity index 100% rename from src/commands/unused/execute_tests.rs rename to cli/src/commands/unused/execute_tests.rs diff --git a/src/commands/unused/mod.rs b/cli/src/commands/unused/mod.rs similarity index 98% rename from src/commands/unused/mod.rs rename to cli/src/commands/unused/mod.rs index 2304e99..0c9f159 100644 --- a/src/commands/unused/mod.rs +++ b/cli/src/commands/unused/mod.rs @@ -7,7 +7,7 @@ mod output_tests; use std::error::Error; use clap::Args; -use cozo::DbInstance; +use db::DbInstance; use crate::commands::{CommandRunner, CommonArgs, Execute}; use crate::output::{OutputFormat, Outputable}; diff --git a/src/commands/unused/output.rs b/cli/src/commands/unused/output.rs similarity index 96% rename from src/commands/unused/output.rs rename to cli/src/commands/unused/output.rs index 2ca5e6b..5b4c78f 100644 --- a/src/commands/unused/output.rs +++ b/cli/src/commands/unused/output.rs @@ -1,7 +1,7 @@ //! Output formatting for unused command results. use crate::output::Outputable; -use crate::types::ModuleCollectionResult; +use db::types::ModuleCollectionResult; use super::execute::UnusedFunc; impl Outputable for ModuleCollectionResult { diff --git a/src/commands/unused/output_tests.rs b/cli/src/commands/unused/output_tests.rs similarity index 92% rename from src/commands/unused/output_tests.rs rename to cli/src/commands/unused/output_tests.rs index 096bb96..3e02a18 100644 --- a/src/commands/unused/output_tests.rs +++ b/cli/src/commands/unused/output_tests.rs @@ -3,7 +3,7 @@ #[cfg(test)] mod tests { use super::super::execute::UnusedFunc; - use crate::types::{ModuleCollectionResult, ModuleGroup}; + use db::types::{ModuleCollectionResult, ModuleGroup}; use rstest::{fixture, rstest}; // ========================================================================= @@ -121,7 +121,7 @@ MyApp.Accounts (lib/accounts.ex): test_name: test_format_json, fixture: single_result, fixture_type: ModuleCollectionResult, - expected: crate::test_utils::load_output_fixture("unused", "single.json"), + expected: db::test_utils::load_output_fixture("unused", "single.json"), format: Json, } @@ -129,7 +129,7 @@ MyApp.Accounts (lib/accounts.ex): test_name: test_format_toon, fixture: single_result, fixture_type: ModuleCollectionResult, - expected: crate::test_utils::load_output_fixture("unused", "single.toon"), + expected: db::test_utils::load_output_fixture("unused", "single.toon"), format: Toon, } @@ -137,7 +137,7 @@ MyApp.Accounts (lib/accounts.ex): test_name: test_format_toon_empty, fixture: empty_result, fixture_type: ModuleCollectionResult, - expected: crate::test_utils::load_output_fixture("unused", "empty.toon"), + expected: db::test_utils::load_output_fixture("unused", "empty.toon"), format: Toon, } } diff --git a/src/dedup.rs b/cli/src/dedup.rs similarity index 100% rename from src/dedup.rs rename to cli/src/dedup.rs diff --git a/src/main.rs b/cli/src/main.rs similarity index 70% rename from src/main.rs rename to cli/src/main.rs index 63a857c..06b8ba4 100644 --- a/src/main.rs +++ b/cli/src/main.rs @@ -2,31 +2,25 @@ use clap::Parser; mod cli; mod commands; -mod db; mod dedup; pub mod output; -mod queries; -pub mod types; mod utils; #[macro_use] mod test_macros; -#[cfg(test)] -pub mod fixtures; -#[cfg(test)] -pub mod test_utils; use cli::Args; use commands::CommandRunner; +use db::open_db; fn main() -> Result<(), Box> { let args = Args::parse(); let db_path = cli::resolve_db_path(args.db); // Create .code_search directory if using default path - if db_path == std::path::PathBuf::from(".code_search/cozo.sqlite") { + if db_path.as_path() == std::path::Path::new(".code_search/cozo.sqlite") { std::fs::create_dir_all(".code_search").ok(); } - let db = db::open_db(&db_path)?; + let db = open_db(&db_path)?; let output = args.command.run(&db, args.format)?; println!("{}", output); Ok(()) diff --git a/src/output.rs b/cli/src/output.rs similarity index 96% rename from src/output.rs rename to cli/src/output.rs index c52beb6..5f6b953 100644 --- a/src/output.rs +++ b/cli/src/output.rs @@ -4,7 +4,7 @@ use clap::ValueEnum; use serde::Serialize; -use crate::types::{ModuleGroupResult, ModuleCollectionResult}; +use db::types::{ModuleGroupResult, ModuleCollectionResult}; /// Output format for command results #[derive(Debug, Clone, Copy, Default, ValueEnum)] @@ -118,7 +118,7 @@ pub trait TableFormatter { /// /// This is the shared implementation for both ModuleGroupResult and ModuleCollectionResult. /// Extracts the common logic to avoid duplication between the two impl blocks. -fn format_module_table(formatter: &F, items: &[crate::types::ModuleGroup], total_items: usize) -> String +fn format_module_table(formatter: &F, items: &[db::types::ModuleGroup], total_items: usize) -> String where F: TableFormatter, { diff --git a/src/test_macros.rs b/cli/src/test_macros.rs similarity index 86% rename from src/test_macros.rs rename to cli/src/test_macros.rs index 021ee6d..3300d75 100644 --- a/src/test_macros.rs +++ b/cli/src/test_macros.rs @@ -19,7 +19,7 @@ macro_rules! cli_defaults_test { fn test_defaults() { let args = Args::try_parse_from(["code_search", $cmd, $($req_arg),*]).unwrap(); match args.command { - crate::commands::Command::$variant(cmd) => { + $crate::commands::Command::$variant(cmd) => { $( assert_eq!(cmd.$($def_field).+, $def_expected, concat!("Default value mismatch for field: ", stringify!($($def_field).+))); @@ -50,7 +50,7 @@ macro_rules! cli_option_test { $($arg),+ ]).unwrap(); match args.command { - crate::commands::Command::$variant(cmd) => { + $crate::commands::Command::$variant(cmd) => { assert_eq!(cmd.$($field).+, $expected, concat!("Field ", stringify!($($field).+), " mismatch")); } @@ -81,7 +81,7 @@ macro_rules! cli_option_test_with_required { $($arg),+ ]).unwrap(); match args.command { - crate::commands::Command::$variant(cmd) => { + $crate::commands::Command::$variant(cmd) => { assert_eq!(cmd.$($field).+, $expected, concat!("Field ", stringify!($($field).+), " mismatch")); } @@ -108,7 +108,7 @@ macro_rules! cli_limit_tests { fn test_limit_default() { let args = Args::try_parse_from(["code_search", $cmd, $($req_arg),*]).unwrap(); match args.command { - crate::commands::Command::$variant(cmd) => { + $crate::commands::Command::$variant(cmd) => { assert_eq!(cmd.$($limit_field).+, $limit_default); } _ => panic!(concat!("Expected ", stringify!($variant), " command")), @@ -219,8 +219,8 @@ macro_rules! execute_test_fixture { project: $project:literal $(,)? ) => { #[fixture] - fn $name() -> cozo::DbInstance { - crate::test_utils::setup_test_db($json, $project) + fn $name() -> db::DbInstance { + db::test_utils::setup_test_db($json, $project) } }; } @@ -245,8 +245,8 @@ macro_rules! shared_fixture { project: $project:literal $(,)? ) => { #[fixture] - fn $name() -> cozo::DbInstance { - crate::test_utils::call_graph_db($project) + fn $name() -> db::DbInstance { + db::test_utils::call_graph_db($project) } }; ( @@ -255,8 +255,8 @@ macro_rules! shared_fixture { project: $project:literal $(,)? ) => { #[fixture] - fn $name() -> cozo::DbInstance { - crate::test_utils::type_signatures_db($project) + fn $name() -> db::DbInstance { + db::test_utils::type_signatures_db($project) } }; ( @@ -265,8 +265,8 @@ macro_rules! shared_fixture { project: $project:literal $(,)? ) => { #[fixture] - fn $name() -> cozo::DbInstance { - crate::test_utils::structs_db($project) + fn $name() -> db::DbInstance { + db::test_utils::structs_db($project) } }; } @@ -280,7 +280,9 @@ macro_rules! execute_empty_db_test { ) => { #[rstest] fn test_empty_db() { - let result = crate::test_utils::execute_on_empty_db($cmd); + use $crate::commands::Execute; + let db = db::test_utils::setup_empty_test_db(); + let result = $cmd.execute(&db); assert!(result.is_err()); } }; @@ -318,8 +320,8 @@ macro_rules! execute_test { assertions: |$result:ident| $assertions:expr $(,)? ) => { #[rstest] - fn $test_name($fixture: cozo::DbInstance) { - use crate::commands::Execute; + fn $test_name($fixture: db::DbInstance) { + use $crate::commands::Execute; let $result = $cmd.execute(&$fixture).expect("Execute should succeed"); $assertions } @@ -346,10 +348,13 @@ macro_rules! execute_no_match_test { empty_field: $field:ident $(,)? ) => { #[rstest] - fn $test_name($fixture: cozo::DbInstance) { - use crate::commands::Execute; + fn $test_name($fixture: db::DbInstance) { + use $crate::commands::Execute; let result = $cmd.execute(&$fixture).expect("Execute should succeed"); - assert!(result.$field.is_empty(), concat!(stringify!($field), " should be empty")); + assert!( + result.$field.is_empty(), + concat!(stringify!($field), " should be empty") + ); } }; } @@ -376,11 +381,14 @@ macro_rules! execute_count_test { expected: $expected:expr $(,)? ) => { #[rstest] - fn $test_name($fixture: cozo::DbInstance) { - use crate::commands::Execute; + fn $test_name($fixture: db::DbInstance) { + use $crate::commands::Execute; let result = $cmd.execute(&$fixture).expect("Execute should succeed"); - assert_eq!(result.$field.len(), $expected, - concat!("Expected ", stringify!($expected), " ", stringify!($field))); + assert_eq!( + result.$field.len(), + $expected, + concat!("Expected ", stringify!($expected), " ", stringify!($field)) + ); } }; } @@ -407,11 +415,13 @@ macro_rules! execute_field_test { expected: $expected:expr $(,)? ) => { #[rstest] - fn $test_name($fixture: cozo::DbInstance) { - use crate::commands::Execute; + fn $test_name($fixture: db::DbInstance) { + use $crate::commands::Execute; let result = $cmd.execute(&$fixture).expect("Execute should succeed"); - assert_eq!(result.$field, $expected, - concat!("Field ", stringify!($field), " mismatch")); + assert_eq!( + result.$field, $expected, + concat!("Field ", stringify!($field), " mismatch") + ); } }; } @@ -440,12 +450,17 @@ macro_rules! execute_first_item_test { expected: $expected:expr $(,)? ) => { #[rstest] - fn $test_name($fixture: cozo::DbInstance) { - use crate::commands::Execute; + fn $test_name($fixture: db::DbInstance) { + use $crate::commands::Execute; let result = $cmd.execute(&$fixture).expect("Execute should succeed"); - assert!(!result.$collection.is_empty(), concat!(stringify!($collection), " should not be empty")); - assert_eq!(result.$collection[0].$field, $expected, - concat!("First item ", stringify!($field), " mismatch")); + assert!( + !result.$collection.is_empty(), + concat!(stringify!($collection), " should not be empty") + ); + assert_eq!( + result.$collection[0].$field, $expected, + concat!("First item ", stringify!($field), " mismatch") + ); } }; } @@ -472,11 +487,13 @@ macro_rules! execute_all_match_test { condition: |$item:ident| $cond:expr $(,)? ) => { #[rstest] - fn $test_name($fixture: cozo::DbInstance) { - use crate::commands::Execute; + fn $test_name($fixture: db::DbInstance) { + use $crate::commands::Execute; let result = $cmd.execute(&$fixture).expect("Execute should succeed"); - assert!(result.$collection.iter().all(|$item| $cond), - concat!("Not all ", stringify!($collection), " matched condition")); + assert!( + result.$collection.iter().all(|$item| $cond), + concat!("Not all ", stringify!($collection), " matched condition") + ); } }; } @@ -503,11 +520,18 @@ macro_rules! execute_limit_test { limit: $limit:expr $(,)? ) => { #[rstest] - fn $test_name($fixture: cozo::DbInstance) { - use crate::commands::Execute; + fn $test_name($fixture: db::DbInstance) { + use $crate::commands::Execute; let result = $cmd.execute(&$fixture).expect("Execute should succeed"); - assert!(result.$collection.len() <= $limit, - concat!("Expected at most ", stringify!($limit), " ", stringify!($collection))); + assert!( + result.$collection.len() <= $limit, + concat!( + "Expected at most ", + stringify!($limit), + " ", + stringify!($collection) + ) + ); } }; } @@ -541,7 +565,7 @@ macro_rules! output_table_test { ) => { #[rstest] fn $test_name($fixture: $fixture_type) { - use crate::output::{Outputable, OutputFormat}; + use $crate::output::{OutputFormat, Outputable}; assert_eq!($fixture.format(OutputFormat::$format), $expected); } }; @@ -554,7 +578,7 @@ macro_rules! output_table_test { ) => { #[rstest] fn $test_name($fixture: $fixture_type) { - use crate::output::Outputable; + use $crate::output::Outputable; assert_eq!($fixture.to_table(), $expected); } }; @@ -573,7 +597,7 @@ macro_rules! output_table_contains_test { ) => { #[rstest] fn $test_name($fixture: $fixture_type) { - use crate::output::Outputable; + use $crate::output::Outputable; let output = $fixture.to_table(); $( assert!(output.contains($needle), concat!("Table output should contain: ", $needle)); @@ -606,7 +630,7 @@ macro_rules! output_json_test { ) => { #[rstest] fn $test_name($fixture: $fixture_type) { - use crate::output::{Outputable, OutputFormat}; + use $crate::output::{Outputable, OutputFormat}; let output = $fixture.format(OutputFormat::Json); let parsed: serde_json::Value = serde_json::from_str(&output) .expect("Should produce valid JSON"); @@ -638,7 +662,7 @@ macro_rules! output_toon_test { ) => { #[rstest] fn $test_name($fixture: $fixture_type) { - use crate::output::{Outputable, OutputFormat}; + use $crate::output::{Outputable, OutputFormat}; let output = $fixture.format(OutputFormat::Toon); $( assert!(output.contains($needle), concat!("Toon output should contain: ", $needle)); diff --git a/src/utils.rs b/cli/src/utils.rs similarity index 70% rename from src/utils.rs rename to cli/src/utils.rs index 6bd1d3e..b156fef 100644 --- a/src/utils.rs +++ b/cli/src/utils.rs @@ -1,158 +1,10 @@ -//! Utility functions for code search operations. +//! Utility functions for code search CLI output and presentation. use std::collections::BTreeMap; use regex::Regex; -use crate::types::{ModuleGroup, Call}; +use db::types::{ModuleGroup, Call}; use crate::dedup::sort_and_deduplicate; -/// Builds SQL WHERE clause conditions for query patterns (exact or regex matching) -/// -/// Handles the common pattern of building conditions that differ between exact and regex modes. -/// Supports different field prefixes and optional leading comma. -/// -/// # Examples -/// -/// ```ignore -/// let builder = ConditionBuilder::new("module", "module_pattern"); -/// let cond = builder.build(false); // "module == $module_pattern" -/// let cond = builder.build(true); // "regex_matches(module, $module_pattern)" -/// ``` -pub struct ConditionBuilder { - field_name: String, - param_name: String, - with_leading_comma: bool, -} - -impl ConditionBuilder { - /// Creates a new condition builder for a field with exact/regex matching - /// - /// # Arguments - /// * `field_name` - The SQL field name (e.g., "module", "caller_module") - /// * `param_name` - The parameter name (e.g., "module_pattern", "function_pattern") - pub fn new(field_name: &str, param_name: &str) -> Self { - Self { - field_name: field_name.to_string(), - param_name: param_name.to_string(), - with_leading_comma: false, - } - } - - /// Adds a leading comma to the condition (useful for mid-query conditions) - pub fn with_leading_comma(mut self) -> Self { - self.with_leading_comma = true; - self - } - - /// Builds the condition string based on use_regex flag - /// - /// When `use_regex` is true, uses `regex_matches()`. - /// When `use_regex` is false, uses exact matching with `==`. - /// - /// # Arguments - /// * `use_regex` - Whether to use regex matching - /// - /// # Returns - /// A condition string ready to be interpolated into a SQL query - pub fn build(&self, use_regex: bool) -> String { - let prefix = if self.with_leading_comma { ", " } else { "" }; - - if use_regex { - format!( - "{}regex_matches({}, ${})", - prefix, self.field_name, self.param_name - ) - } else { - format!("{}{} == ${}", prefix, self.field_name, self.param_name) - } - } -} - -/// Builder for optional SQL conditions (function, arity, etc.) -/// -/// Handles the pattern of generating conditions only when values are present. -/// For function-matching conditions, supports both exact and regex matching. -pub struct OptionalConditionBuilder { - field_name: String, - param_name: String, - with_leading_comma: bool, - when_none: Option, // Alternative condition when value is None - supports_regex: bool, // Whether to use regex_matches when value is present -} - -impl OptionalConditionBuilder { - /// Creates a new optional condition builder - /// - /// # Arguments - /// * `field_name` - The SQL field name - /// * `param_name` - The parameter name - pub fn new(field_name: &str, param_name: &str) -> Self { - Self { - field_name: field_name.to_string(), - param_name: param_name.to_string(), - with_leading_comma: false, - when_none: None, - supports_regex: false, - } - } - - /// Enables regex matching (uses regex_matches when value is present) - pub fn with_regex(mut self) -> Self { - self.supports_regex = true; - self - } - - /// Adds a leading comma - pub fn with_leading_comma(mut self) -> Self { - self.with_leading_comma = true; - self - } - - /// Sets an alternative condition when the value is None (e.g., "true" for no-op) - pub fn when_none(mut self, condition: &str) -> Self { - self.when_none = Some(condition.to_string()); - self - } - - /// Builds the condition string - /// - /// # Arguments - /// * `has_value` - Whether the optional value is present - /// * `use_regex` - Whether to use regex matching (only matters if supports_regex is true) - /// - /// # Returns - /// A condition string, or empty string if no value and no alternative - pub fn build_with_regex(&self, has_value: bool, use_regex: bool) -> String { - let prefix = if self.with_leading_comma { ", " } else { "" }; - - if has_value { - if self.supports_regex && use_regex { - format!( - "{}regex_matches({}, ${})", - prefix, self.field_name, self.param_name - ) - } else { - format!("{}{} == ${}", prefix, self.field_name, self.param_name) - } - } else { - self.when_none - .as_ref() - .map(|cond| format!("{}{}", prefix, cond)) - .unwrap_or_default() - } - } - - /// Builds the condition string (non-regex version, for backward compatibility) - /// - /// # Arguments - /// * `has_value` - Whether the optional value is present - /// - /// # Returns - /// A condition string, or empty string if no value and no alternative - pub fn build(&self, has_value: bool) -> String { - self.build_with_regex(has_value, false) - } -} - /// Groups items by module into a structured result /// /// Transforms a vector of source items into (module, entry) tuples and groups them by module @@ -635,50 +487,4 @@ mod tests { assert_eq!(result[1].entries.len(), 2); // math has 2 items assert_eq!(result[2].entries.len(), 2); // string has 2 items } - - #[test] - fn test_condition_builder_exact_match() { - let builder = ConditionBuilder::new("module", "module_pattern"); - assert_eq!(builder.build(false), "module == $module_pattern"); - } - - #[test] - fn test_condition_builder_regex_match() { - let builder = ConditionBuilder::new("module", "module_pattern"); - assert_eq!(builder.build(true), "regex_matches(module, $module_pattern)"); - } - - #[test] - fn test_condition_builder_with_leading_comma() { - let builder = ConditionBuilder::new("module", "module_pattern").with_leading_comma(); - assert_eq!(builder.build(false), ", module == $module_pattern"); - assert_eq!(builder.build(true), ", regex_matches(module, $module_pattern)"); - } - - #[test] - fn test_optional_condition_builder_with_value() { - let builder = OptionalConditionBuilder::new("arity", "arity"); - assert_eq!(builder.build(true), "arity == $arity"); - } - - #[test] - fn test_optional_condition_builder_without_value() { - let builder = OptionalConditionBuilder::new("arity", "arity"); - assert_eq!(builder.build(false), ""); - } - - #[test] - fn test_optional_condition_builder_with_default() { - let builder = OptionalConditionBuilder::new("arity", "arity").when_none("true"); - assert_eq!(builder.build(false), "true"); - } - - #[test] - fn test_optional_condition_builder_with_leading_comma() { - let builder = OptionalConditionBuilder::new("arity", "arity") - .with_leading_comma() - .when_none("true"); - assert_eq!(builder.build(true), ", arity == $arity"); - assert_eq!(builder.build(false), ", true"); - } } diff --git a/db/Cargo.toml b/db/Cargo.toml new file mode 100644 index 0000000..9f4c604 --- /dev/null +++ b/db/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "db" +version.workspace = true +edition.workspace = true + +[dependencies] +cozo = { version = "0.7.6", default-features = false, features = ["compact", "storage-sqlite"] } +serde = { version = "1.0", features = ["derive"] } +thiserror = "1.0" +regex = "1" +include_dir = "0.7" +clap = { version = "4", features = ["derive"] } +tempfile = { version = "3", optional = true } +serde_json = { version = "1.0", optional = true } + +[dev-dependencies] +rstest = "0.23" +tempfile = "3" +serde_json = "1.0" + +[features] +test-utils = ["tempfile", "serde_json"] diff --git a/src/db.rs b/db/src/db.rs similarity index 97% rename from src/db.rs rename to db/src/db.rs index e74370d..354acf3 100644 --- a/src/db.rs +++ b/db/src/db.rs @@ -64,7 +64,7 @@ pub fn open_db(path: &Path) -> Result> { /// Create an in-memory database instance. /// /// Used for tests to avoid disk I/O and temp file management. -#[cfg(test)] +#[cfg(any(test, feature = "test-utils"))] pub fn open_mem_db() -> DbInstance { DbInstance::new("mem", "", "").expect("Failed to create in-memory DB") } @@ -259,20 +259,20 @@ impl CallRowLayout { /// (None) if any required string field cannot be extracted. pub fn extract_call_from_row(row: &[DataValue], layout: &CallRowLayout) -> Option { // Extract caller information - let Some(caller_module) = extract_string(&row[layout.caller_module_idx]) else { return None }; - let Some(caller_name) = extract_string(&row[layout.caller_name_idx]) else { return None }; + let caller_module = extract_string(&row[layout.caller_module_idx])?; + let caller_name = extract_string(&row[layout.caller_name_idx])?; let caller_arity = extract_i64(&row[layout.caller_arity_idx], 0); let caller_kind = extract_string_or(&row[layout.caller_kind_idx], ""); let caller_start_line = extract_i64(&row[layout.caller_start_line_idx], 0); let caller_end_line = extract_i64(&row[layout.caller_end_line_idx], 0); // Extract callee information - let Some(callee_module) = extract_string(&row[layout.callee_module_idx]) else { return None }; - let Some(callee_name) = extract_string(&row[layout.callee_name_idx]) else { return None }; + let callee_module = extract_string(&row[layout.callee_module_idx])?; + let callee_name = extract_string(&row[layout.callee_name_idx])?; let callee_arity = extract_i64(&row[layout.callee_arity_idx], 0); // Extract file and line - let Some(file) = extract_string(&row[layout.file_idx]) else { return None }; + let file = extract_string(&row[layout.file_idx])?; let line = extract_i64(&row[layout.line_idx], 0); // Extract optional call_type diff --git a/src/fixtures/call_graph.json b/db/src/fixtures/call_graph.json similarity index 100% rename from src/fixtures/call_graph.json rename to db/src/fixtures/call_graph.json diff --git a/src/fixtures/extracted_trace.json b/db/src/fixtures/extracted_trace.json similarity index 100% rename from src/fixtures/extracted_trace.json rename to db/src/fixtures/extracted_trace.json diff --git a/src/fixtures/mod.rs b/db/src/fixtures/mod.rs similarity index 100% rename from src/fixtures/mod.rs rename to db/src/fixtures/mod.rs diff --git a/src/fixtures/output/calls_from/empty.toon b/db/src/fixtures/output/calls_from/empty.toon similarity index 100% rename from src/fixtures/output/calls_from/empty.toon rename to db/src/fixtures/output/calls_from/empty.toon diff --git a/src/fixtures/output/calls_from/single.json b/db/src/fixtures/output/calls_from/single.json similarity index 100% rename from src/fixtures/output/calls_from/single.json rename to db/src/fixtures/output/calls_from/single.json diff --git a/src/fixtures/output/calls_from/single.toon b/db/src/fixtures/output/calls_from/single.toon similarity index 100% rename from src/fixtures/output/calls_from/single.toon rename to db/src/fixtures/output/calls_from/single.toon diff --git a/src/fixtures/output/calls_to/empty.toon b/db/src/fixtures/output/calls_to/empty.toon similarity index 100% rename from src/fixtures/output/calls_to/empty.toon rename to db/src/fixtures/output/calls_to/empty.toon diff --git a/src/fixtures/output/calls_to/single.json b/db/src/fixtures/output/calls_to/single.json similarity index 100% rename from src/fixtures/output/calls_to/single.json rename to db/src/fixtures/output/calls_to/single.json diff --git a/src/fixtures/output/calls_to/single.toon b/db/src/fixtures/output/calls_to/single.toon similarity index 100% rename from src/fixtures/output/calls_to/single.toon rename to db/src/fixtures/output/calls_to/single.toon diff --git a/src/fixtures/output/depended_by/empty.toon b/db/src/fixtures/output/depended_by/empty.toon similarity index 100% rename from src/fixtures/output/depended_by/empty.toon rename to db/src/fixtures/output/depended_by/empty.toon diff --git a/src/fixtures/output/depended_by/single.json b/db/src/fixtures/output/depended_by/single.json similarity index 100% rename from src/fixtures/output/depended_by/single.json rename to db/src/fixtures/output/depended_by/single.json diff --git a/src/fixtures/output/depended_by/single.toon b/db/src/fixtures/output/depended_by/single.toon similarity index 100% rename from src/fixtures/output/depended_by/single.toon rename to db/src/fixtures/output/depended_by/single.toon diff --git a/src/fixtures/output/depends_on/empty.toon b/db/src/fixtures/output/depends_on/empty.toon similarity index 100% rename from src/fixtures/output/depends_on/empty.toon rename to db/src/fixtures/output/depends_on/empty.toon diff --git a/src/fixtures/output/depends_on/single.json b/db/src/fixtures/output/depends_on/single.json similarity index 100% rename from src/fixtures/output/depends_on/single.json rename to db/src/fixtures/output/depends_on/single.json diff --git a/src/fixtures/output/depends_on/single.toon b/db/src/fixtures/output/depends_on/single.toon similarity index 100% rename from src/fixtures/output/depends_on/single.toon rename to db/src/fixtures/output/depends_on/single.toon diff --git a/src/fixtures/output/function/empty.toon b/db/src/fixtures/output/function/empty.toon similarity index 100% rename from src/fixtures/output/function/empty.toon rename to db/src/fixtures/output/function/empty.toon diff --git a/src/fixtures/output/function/single.json b/db/src/fixtures/output/function/single.json similarity index 100% rename from src/fixtures/output/function/single.json rename to db/src/fixtures/output/function/single.json diff --git a/src/fixtures/output/function/single.toon b/db/src/fixtures/output/function/single.toon similarity index 100% rename from src/fixtures/output/function/single.toon rename to db/src/fixtures/output/function/single.toon diff --git a/src/fixtures/output/hotspots/empty.toon b/db/src/fixtures/output/hotspots/empty.toon similarity index 100% rename from src/fixtures/output/hotspots/empty.toon rename to db/src/fixtures/output/hotspots/empty.toon diff --git a/src/fixtures/output/hotspots/single.json b/db/src/fixtures/output/hotspots/single.json similarity index 100% rename from src/fixtures/output/hotspots/single.json rename to db/src/fixtures/output/hotspots/single.json diff --git a/src/fixtures/output/hotspots/single.toon b/db/src/fixtures/output/hotspots/single.toon similarity index 100% rename from src/fixtures/output/hotspots/single.toon rename to db/src/fixtures/output/hotspots/single.toon diff --git a/src/fixtures/output/import/full.json b/db/src/fixtures/output/import/full.json similarity index 100% rename from src/fixtures/output/import/full.json rename to db/src/fixtures/output/import/full.json diff --git a/src/fixtures/output/import/full.toon b/db/src/fixtures/output/import/full.toon similarity index 100% rename from src/fixtures/output/import/full.toon rename to db/src/fixtures/output/import/full.toon diff --git a/src/fixtures/output/location/empty.toon b/db/src/fixtures/output/location/empty.toon similarity index 100% rename from src/fixtures/output/location/empty.toon rename to db/src/fixtures/output/location/empty.toon diff --git a/src/fixtures/output/location/single.json b/db/src/fixtures/output/location/single.json similarity index 100% rename from src/fixtures/output/location/single.json rename to db/src/fixtures/output/location/single.json diff --git a/src/fixtures/output/location/single.toon b/db/src/fixtures/output/location/single.toon similarity index 100% rename from src/fixtures/output/location/single.toon rename to db/src/fixtures/output/location/single.toon diff --git a/src/fixtures/output/path/empty.toon b/db/src/fixtures/output/path/empty.toon similarity index 100% rename from src/fixtures/output/path/empty.toon rename to db/src/fixtures/output/path/empty.toon diff --git a/src/fixtures/output/path/single.json b/db/src/fixtures/output/path/single.json similarity index 100% rename from src/fixtures/output/path/single.json rename to db/src/fixtures/output/path/single.json diff --git a/src/fixtures/output/path/single.toon b/db/src/fixtures/output/path/single.toon similarity index 100% rename from src/fixtures/output/path/single.toon rename to db/src/fixtures/output/path/single.toon diff --git a/src/fixtures/output/reverse_trace/empty.toon b/db/src/fixtures/output/reverse_trace/empty.toon similarity index 100% rename from src/fixtures/output/reverse_trace/empty.toon rename to db/src/fixtures/output/reverse_trace/empty.toon diff --git a/src/fixtures/output/reverse_trace/single.json b/db/src/fixtures/output/reverse_trace/single.json similarity index 100% rename from src/fixtures/output/reverse_trace/single.json rename to db/src/fixtures/output/reverse_trace/single.json diff --git a/src/fixtures/output/reverse_trace/single.toon b/db/src/fixtures/output/reverse_trace/single.toon similarity index 100% rename from src/fixtures/output/reverse_trace/single.toon rename to db/src/fixtures/output/reverse_trace/single.toon diff --git a/src/fixtures/output/search/empty.toon b/db/src/fixtures/output/search/empty.toon similarity index 100% rename from src/fixtures/output/search/empty.toon rename to db/src/fixtures/output/search/empty.toon diff --git a/src/fixtures/output/search/modules.json b/db/src/fixtures/output/search/modules.json similarity index 100% rename from src/fixtures/output/search/modules.json rename to db/src/fixtures/output/search/modules.json diff --git a/src/fixtures/output/search/modules.toon b/db/src/fixtures/output/search/modules.toon similarity index 100% rename from src/fixtures/output/search/modules.toon rename to db/src/fixtures/output/search/modules.toon diff --git a/src/fixtures/output/trace/empty.toon b/db/src/fixtures/output/trace/empty.toon similarity index 100% rename from src/fixtures/output/trace/empty.toon rename to db/src/fixtures/output/trace/empty.toon diff --git a/src/fixtures/output/trace/single.json b/db/src/fixtures/output/trace/single.json similarity index 100% rename from src/fixtures/output/trace/single.json rename to db/src/fixtures/output/trace/single.json diff --git a/src/fixtures/output/trace/single.toon b/db/src/fixtures/output/trace/single.toon similarity index 100% rename from src/fixtures/output/trace/single.toon rename to db/src/fixtures/output/trace/single.toon diff --git a/src/fixtures/output/unused/empty.toon b/db/src/fixtures/output/unused/empty.toon similarity index 100% rename from src/fixtures/output/unused/empty.toon rename to db/src/fixtures/output/unused/empty.toon diff --git a/src/fixtures/output/unused/single.json b/db/src/fixtures/output/unused/single.json similarity index 100% rename from src/fixtures/output/unused/single.json rename to db/src/fixtures/output/unused/single.json diff --git a/src/fixtures/output/unused/single.toon b/db/src/fixtures/output/unused/single.toon similarity index 100% rename from src/fixtures/output/unused/single.toon rename to db/src/fixtures/output/unused/single.toon diff --git a/src/fixtures/structs.json b/db/src/fixtures/structs.json similarity index 100% rename from src/fixtures/structs.json rename to db/src/fixtures/structs.json diff --git a/src/fixtures/type_signatures.json b/db/src/fixtures/type_signatures.json similarity index 100% rename from src/fixtures/type_signatures.json rename to db/src/fixtures/type_signatures.json diff --git a/db/src/lib.rs b/db/src/lib.rs new file mode 100644 index 0000000..491f1e4 --- /dev/null +++ b/db/src/lib.rs @@ -0,0 +1,27 @@ +//! Database layer for code search - CozoDB queries and call graph data structures + +pub mod db; +pub mod types; +pub mod query_builders; +pub mod queries; + +#[cfg(feature = "test-utils")] +pub mod test_utils; + +#[cfg(feature = "test-utils")] +pub mod fixtures; + +// Re-export commonly used items +pub use db::{open_db, run_query, run_query_no_params, DbError, Params}; +pub use cozo::DbInstance; + +#[cfg(any(test, feature = "test-utils"))] +pub use db::open_mem_db; + +pub use types::{ + Call, FunctionRef, ModuleGroup, ModuleGroupResult, + ModuleCollectionResult, TraceResult, TraceEntry, + TraceDirection, SharedStr +}; + +pub use query_builders::{ConditionBuilder, OptionalConditionBuilder}; diff --git a/src/queries/accepts.rs b/db/src/queries/accepts.rs similarity index 100% rename from src/queries/accepts.rs rename to db/src/queries/accepts.rs diff --git a/src/queries/calls.rs b/db/src/queries/calls.rs similarity index 93% rename from src/queries/calls.rs rename to db/src/queries/calls.rs index 0d42eec..bb668cf 100644 --- a/src/queries/calls.rs +++ b/db/src/queries/calls.rs @@ -11,6 +11,7 @@ use thiserror::Error; use crate::db::{extract_call_from_row, run_query, CallRowLayout, Params}; use crate::types::Call; +use crate::query_builders::{ConditionBuilder, OptionalConditionBuilder}; #[derive(Error, Debug)] pub enum CallsError { @@ -68,13 +69,13 @@ pub fn find_calls( // Build conditions using the appropriate field names let module_cond = - crate::utils::ConditionBuilder::new(module_field, "module_pattern").build(use_regex); + ConditionBuilder::new(module_field, "module_pattern").build(use_regex); let function_cond = - crate::utils::OptionalConditionBuilder::new(function_field, "function_pattern") + OptionalConditionBuilder::new(function_field, "function_pattern") .with_leading_comma() .with_regex() .build_with_regex(function_pattern.is_some(), use_regex); - let arity_cond = crate::utils::OptionalConditionBuilder::new(arity_field, "arity") + let arity_cond = OptionalConditionBuilder::new(arity_field, "arity") .with_leading_comma() .build(arity.is_some()); diff --git a/src/queries/calls_from.rs b/db/src/queries/calls_from.rs similarity index 100% rename from src/queries/calls_from.rs rename to db/src/queries/calls_from.rs diff --git a/src/queries/calls_to.rs b/db/src/queries/calls_to.rs similarity index 100% rename from src/queries/calls_to.rs rename to db/src/queries/calls_to.rs diff --git a/src/queries/clusters.rs b/db/src/queries/clusters.rs similarity index 100% rename from src/queries/clusters.rs rename to db/src/queries/clusters.rs diff --git a/src/queries/complexity.rs b/db/src/queries/complexity.rs similarity index 100% rename from src/queries/complexity.rs rename to db/src/queries/complexity.rs diff --git a/src/queries/cycles.rs b/db/src/queries/cycles.rs similarity index 95% rename from src/queries/cycles.rs rename to db/src/queries/cycles.rs index 47a6105..62568c8 100644 --- a/src/queries/cycles.rs +++ b/db/src/queries/cycles.rs @@ -77,11 +77,10 @@ pub fn find_cycle_edges( (row.get(from_idx), row.get(to_idx)) { // Apply module pattern filter if provided - if let Some(pattern) = module_pattern { - if !from.contains(pattern) && !to.contains(pattern) { + if let Some(pattern) = module_pattern + && !from.contains(pattern) && !to.contains(pattern) { continue; } - } edges.push(CycleEdge { from: from.to_string(), to: to.to_string(), diff --git a/src/queries/depended_by.rs b/db/src/queries/depended_by.rs similarity index 100% rename from src/queries/depended_by.rs rename to db/src/queries/depended_by.rs diff --git a/src/queries/dependencies.rs b/db/src/queries/dependencies.rs similarity index 97% rename from src/queries/dependencies.rs rename to db/src/queries/dependencies.rs index 49225a0..7266197 100644 --- a/src/queries/dependencies.rs +++ b/db/src/queries/dependencies.rs @@ -11,6 +11,7 @@ use thiserror::Error; use crate::db::{extract_call_from_row, run_query, CallRowLayout, Params}; use crate::types::Call; +use crate::query_builders::ConditionBuilder; #[derive(Error, Debug)] pub enum DependencyError { @@ -70,7 +71,7 @@ pub fn find_dependencies( // Build module condition using the appropriate field name let module_cond = - crate::utils::ConditionBuilder::new(filter_field, "module_pattern").build(use_regex); + ConditionBuilder::new(filter_field, "module_pattern").build(use_regex); // Query calls with function_locations join for caller metadata, excluding self-references // Filter out struct calls (callee_function != '%') diff --git a/src/queries/depends_on.rs b/db/src/queries/depends_on.rs similarity index 100% rename from src/queries/depends_on.rs rename to db/src/queries/depends_on.rs diff --git a/src/queries/duplicates.rs b/db/src/queries/duplicates.rs similarity index 100% rename from src/queries/duplicates.rs rename to db/src/queries/duplicates.rs diff --git a/src/queries/file.rs b/db/src/queries/file.rs similarity index 100% rename from src/queries/file.rs rename to db/src/queries/file.rs diff --git a/src/queries/function.rs b/db/src/queries/function.rs similarity index 89% rename from src/queries/function.rs rename to db/src/queries/function.rs index 08dbc13..2ff9f94 100644 --- a/src/queries/function.rs +++ b/db/src/queries/function.rs @@ -5,6 +5,7 @@ use serde::Serialize; use thiserror::Error; use crate::db::{extract_i64, extract_string, extract_string_or, run_query, Params}; +use crate::query_builders::{ConditionBuilder, OptionalConditionBuilder}; #[derive(Error, Debug)] pub enum FunctionError { @@ -33,11 +34,11 @@ pub fn find_functions( limit: u32, ) -> Result, Box> { // Build query conditions using helpers - let module_cond = crate::utils::ConditionBuilder::new("module", "module_pattern").build(use_regex); - let function_cond = crate::utils::ConditionBuilder::new("name", "function_pattern") + let module_cond = ConditionBuilder::new("module", "module_pattern").build(use_regex); + let function_cond = ConditionBuilder::new("name", "function_pattern") .with_leading_comma() .build(use_regex); - let arity_cond = crate::utils::OptionalConditionBuilder::new("arity", "arity") + let arity_cond = OptionalConditionBuilder::new("arity", "arity") .with_leading_comma() .build(arity.is_some()); let project_cond = ", project == $project"; diff --git a/src/queries/hotspots.rs b/db/src/queries/hotspots.rs similarity index 98% rename from src/queries/hotspots.rs rename to db/src/queries/hotspots.rs index 5f3a94c..71c73ac 100644 --- a/src/queries/hotspots.rs +++ b/db/src/queries/hotspots.rs @@ -79,12 +79,11 @@ pub fn get_module_loc( let mut loc_map = std::collections::HashMap::new(); for row in rows.rows { - if row.len() >= 2 { - if let Some(module) = extract_string(&row[0]) { + if row.len() >= 2 + && let Some(module) = extract_string(&row[0]) { let loc = extract_i64(&row[1], 0); loc_map.insert(module, loc); } - } } Ok(loc_map) @@ -130,12 +129,11 @@ pub fn get_function_counts( let mut counts = std::collections::HashMap::new(); for row in rows.rows { - if row.len() >= 2 { - if let Some(module) = extract_string(&row[0]) { + if row.len() >= 2 + && let Some(module) = extract_string(&row[0]) { let count = extract_i64(&row[1], 0); counts.insert(module, count); } - } } Ok(counts) diff --git a/src/queries/import.rs b/db/src/queries/import.rs similarity index 99% rename from src/queries/import.rs rename to db/src/queries/import.rs index b7147eb..dcc7ce0 100644 --- a/src/queries/import.rs +++ b/db/src/queries/import.rs @@ -285,7 +285,7 @@ pub fn import_function_locations( let mut rows = Vec::new(); for (module, functions) in &graph.function_locations { - for (_func_key, loc) in functions { + for loc in functions.values() { // Use deserialized fields directly from the JSON let name = &loc.name; let arity = loc.arity; @@ -303,7 +303,7 @@ pub fn import_function_locations( r#"["{}", "{}", "{}", {}, {}, "{}", "{}", {}, "{}", {}, {}, '{}', '{}', "{}", "{}", {}, {}, "{}", "{}"]"#, escaped_project, escape_string(module), - escape_string(&name), + escape_string(name), arity, line, escape_string(loc.file.as_deref().unwrap_or("")), @@ -440,7 +440,7 @@ pub fn import_graph( /// Import a JSON string directly into the database. /// /// Convenience wrapper for tests that parses JSON and calls `import_graph`. -#[cfg(test)] +#[cfg(any(test, feature = "test-utils"))] pub fn import_json_str( db: &DbInstance, content: &str, diff --git a/src/queries/import_models.rs b/db/src/queries/import_models.rs similarity index 100% rename from src/queries/import_models.rs rename to db/src/queries/import_models.rs diff --git a/src/queries/large_functions.rs b/db/src/queries/large_functions.rs similarity index 100% rename from src/queries/large_functions.rs rename to db/src/queries/large_functions.rs diff --git a/src/queries/location.rs b/db/src/queries/location.rs similarity index 100% rename from src/queries/location.rs rename to db/src/queries/location.rs diff --git a/src/queries/many_clauses.rs b/db/src/queries/many_clauses.rs similarity index 100% rename from src/queries/many_clauses.rs rename to db/src/queries/many_clauses.rs diff --git a/src/queries/mod.rs b/db/src/queries/mod.rs similarity index 100% rename from src/queries/mod.rs rename to db/src/queries/mod.rs diff --git a/src/queries/path.rs b/db/src/queries/path.rs similarity index 96% rename from src/queries/path.rs rename to db/src/queries/path.rs index 8ab479e..b602991 100644 --- a/src/queries/path.rs +++ b/db/src/queries/path.rs @@ -6,6 +6,7 @@ use serde::Serialize; use thiserror::Error; use crate::db::{extract_i64, extract_string, run_query, Params}; +use crate::query_builders::OptionalConditionBuilder; #[derive(Error, Debug)] pub enum PathError { @@ -46,11 +47,11 @@ pub fn find_paths( limit: u32, ) -> Result, Box> { // Build conditions using the ConditionBuilder utilities - let from_arity_cond = crate::utils::OptionalConditionBuilder::new("caller_arity", "from_arity") + let from_arity_cond = OptionalConditionBuilder::new("caller_arity", "from_arity") .when_none("true") .build(from_arity.is_some()); - let to_arity_cond = crate::utils::OptionalConditionBuilder::new("callee_arity", "to_arity") + let to_arity_cond = OptionalConditionBuilder::new("callee_arity", "to_arity") .when_none("true") .build(to_arity.is_some()); @@ -199,7 +200,7 @@ fn dfs_find_paths( // Check if we reached the target let at_target = current_edge.callee_module == to_module && current_edge.callee_function == to_function - && to_arity.map_or(true, |a| current_edge.callee_arity == a); + && to_arity.is_none_or(|a| current_edge.callee_arity == a); if at_target { // Found a complete path diff --git a/src/queries/returns.rs b/db/src/queries/returns.rs similarity index 100% rename from src/queries/returns.rs rename to db/src/queries/returns.rs diff --git a/src/queries/reverse_trace.rs b/db/src/queries/reverse_trace.rs similarity index 94% rename from src/queries/reverse_trace.rs rename to db/src/queries/reverse_trace.rs index dfce44c..1c65ea4 100644 --- a/src/queries/reverse_trace.rs +++ b/db/src/queries/reverse_trace.rs @@ -5,6 +5,7 @@ use serde::Serialize; use thiserror::Error; use crate::db::{extract_i64, extract_string, extract_string_or, run_query, Params}; +use crate::query_builders::{ConditionBuilder, OptionalConditionBuilder}; #[derive(Error, Debug)] pub enum ReverseTraceError { @@ -41,9 +42,9 @@ pub fn reverse_trace_calls( ) -> Result, Box> { // Build the starting conditions for the recursive query using helpers // For reverse trace, we match on the callee (target) - let module_cond = crate::utils::ConditionBuilder::new("callee_module", "module_pattern").build(use_regex); - let function_cond = crate::utils::ConditionBuilder::new("callee_function", "function_pattern").build(use_regex); - let arity_cond = crate::utils::OptionalConditionBuilder::new("callee_arity", "arity") + let module_cond = ConditionBuilder::new("callee_module", "module_pattern").build(use_regex); + let function_cond = ConditionBuilder::new("callee_function", "function_pattern").build(use_regex); + let arity_cond = OptionalConditionBuilder::new("callee_arity", "arity") .when_none("true") .build(arity.is_some()); diff --git a/src/queries/schema.rs b/db/src/queries/schema.rs similarity index 100% rename from src/queries/schema.rs rename to db/src/queries/schema.rs diff --git a/src/queries/search.rs b/db/src/queries/search.rs similarity index 100% rename from src/queries/search.rs rename to db/src/queries/search.rs diff --git a/src/queries/specs.rs b/db/src/queries/specs.rs similarity index 100% rename from src/queries/specs.rs rename to db/src/queries/specs.rs diff --git a/src/queries/struct_usage.rs b/db/src/queries/struct_usage.rs similarity index 100% rename from src/queries/struct_usage.rs rename to db/src/queries/struct_usage.rs diff --git a/src/queries/structs.rs b/db/src/queries/structs.rs similarity index 100% rename from src/queries/structs.rs rename to db/src/queries/structs.rs diff --git a/src/queries/trace.rs b/db/src/queries/trace.rs similarity index 94% rename from src/queries/trace.rs rename to db/src/queries/trace.rs index bbcb7d0..83b6e7f 100644 --- a/src/queries/trace.rs +++ b/db/src/queries/trace.rs @@ -6,6 +6,7 @@ use thiserror::Error; use crate::db::{extract_i64, extract_string, extract_string_or, run_query, Params}; use crate::types::{Call, FunctionRef}; +use crate::query_builders::{ConditionBuilder, OptionalConditionBuilder}; #[derive(Error, Debug)] pub enum TraceError { @@ -24,9 +25,9 @@ pub fn trace_calls( limit: u32, ) -> Result, Box> { // Build the starting conditions for the recursive query using helpers - let module_cond = crate::utils::ConditionBuilder::new("caller_module", "module_pattern").build(use_regex); - let function_cond = crate::utils::ConditionBuilder::new("caller_name", "function_pattern").build(use_regex); - let arity_cond = crate::utils::OptionalConditionBuilder::new("caller_arity", "arity") + let module_cond = ConditionBuilder::new("caller_module", "module_pattern").build(use_regex); + let function_cond = ConditionBuilder::new("caller_name", "function_pattern").build(use_regex); + let arity_cond = OptionalConditionBuilder::new("caller_arity", "arity") .when_none("true") .build(arity.is_some()); diff --git a/src/queries/types.rs b/db/src/queries/types.rs similarity index 100% rename from src/queries/types.rs rename to db/src/queries/types.rs diff --git a/src/queries/unused.rs b/db/src/queries/unused.rs similarity index 100% rename from src/queries/unused.rs rename to db/src/queries/unused.rs diff --git a/db/src/query_builders.rs b/db/src/query_builders.rs new file mode 100644 index 0000000..6ff2609 --- /dev/null +++ b/db/src/query_builders.rs @@ -0,0 +1,202 @@ +//! Query condition builders for CozoScript + +/// Builds SQL WHERE clause conditions for query patterns (exact or regex matching) +/// +/// Handles the common pattern of building conditions that differ between exact and regex modes. +/// Supports different field prefixes and optional leading comma. +/// +/// # Examples +/// +/// ``` +/// use db::query_builders::ConditionBuilder; +/// +/// let builder = ConditionBuilder::new("module", "module_pattern"); +/// let cond = builder.build(false); // "module == $module_pattern" +/// let cond = builder.build(true); // "regex_matches(module, $module_pattern)" +/// ``` +pub struct ConditionBuilder { + field_name: String, + param_name: String, + with_leading_comma: bool, +} + +impl ConditionBuilder { + /// Creates a new condition builder for a field with exact/regex matching + /// + /// # Arguments + /// * `field_name` - The SQL field name (e.g., "module", "caller_module") + /// * `param_name` - The parameter name (e.g., "module_pattern", "function_pattern") + pub fn new(field_name: &str, param_name: &str) -> Self { + Self { + field_name: field_name.to_string(), + param_name: param_name.to_string(), + with_leading_comma: false, + } + } + + /// Adds a leading comma to the condition (useful for mid-query conditions) + pub fn with_leading_comma(mut self) -> Self { + self.with_leading_comma = true; + self + } + + /// Builds the condition string based on use_regex flag + /// + /// When `use_regex` is true, uses `regex_matches()`. + /// When `use_regex` is false, uses exact matching with `==`. + /// + /// # Arguments + /// * `use_regex` - Whether to use regex matching + /// + /// # Returns + /// A condition string ready to be interpolated into a SQL query + pub fn build(&self, use_regex: bool) -> String { + let prefix = if self.with_leading_comma { ", " } else { "" }; + + if use_regex { + format!( + "{}regex_matches({}, ${})", + prefix, self.field_name, self.param_name + ) + } else { + format!("{}{} == ${}", prefix, self.field_name, self.param_name) + } + } +} + +/// Builder for optional SQL conditions (function, arity, etc.) +/// +/// Handles the pattern of generating conditions only when values are present. +/// For function-matching conditions, supports both exact and regex matching. +pub struct OptionalConditionBuilder { + field_name: String, + param_name: String, + with_leading_comma: bool, + when_none: Option, // Alternative condition when value is None + supports_regex: bool, // Whether to use regex_matches when value is present +} + +impl OptionalConditionBuilder { + /// Creates a new optional condition builder + /// + /// # Arguments + /// * `field_name` - The SQL field name + /// * `param_name` - The parameter name + pub fn new(field_name: &str, param_name: &str) -> Self { + Self { + field_name: field_name.to_string(), + param_name: param_name.to_string(), + with_leading_comma: false, + when_none: None, + supports_regex: false, + } + } + + /// Enables regex matching (uses regex_matches when value is present) + pub fn with_regex(mut self) -> Self { + self.supports_regex = true; + self + } + + /// Adds a leading comma + pub fn with_leading_comma(mut self) -> Self { + self.with_leading_comma = true; + self + } + + /// Sets an alternative condition when the value is None (e.g., "true" for no-op) + pub fn when_none(mut self, condition: &str) -> Self { + self.when_none = Some(condition.to_string()); + self + } + + /// Builds the condition string + /// + /// # Arguments + /// * `has_value` - Whether the optional value is present + /// * `use_regex` - Whether to use regex matching (only matters if supports_regex is true) + /// + /// # Returns + /// A condition string, or empty string if no value and no alternative + pub fn build_with_regex(&self, has_value: bool, use_regex: bool) -> String { + let prefix = if self.with_leading_comma { ", " } else { "" }; + + if has_value { + if self.supports_regex && use_regex { + format!( + "{}regex_matches({}, ${})", + prefix, self.field_name, self.param_name + ) + } else { + format!("{}{} == ${}", prefix, self.field_name, self.param_name) + } + } else { + self.when_none + .as_ref() + .map(|cond| format!("{}{}", prefix, cond)) + .unwrap_or_default() + } + } + + /// Builds the condition string (non-regex version, for backward compatibility) + /// + /// # Arguments + /// * `has_value` - Whether the optional value is present + /// + /// # Returns + /// A condition string, or empty string if no value and no alternative + pub fn build(&self, has_value: bool) -> String { + self.build_with_regex(has_value, false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_condition_builder_exact_match() { + let builder = ConditionBuilder::new("module", "module_pattern"); + assert_eq!(builder.build(false), "module == $module_pattern"); + } + + #[test] + fn test_condition_builder_regex_match() { + let builder = ConditionBuilder::new("module", "module_pattern"); + assert_eq!(builder.build(true), "regex_matches(module, $module_pattern)"); + } + + #[test] + fn test_condition_builder_with_leading_comma() { + let builder = ConditionBuilder::new("module", "module_pattern").with_leading_comma(); + assert_eq!(builder.build(false), ", module == $module_pattern"); + assert_eq!(builder.build(true), ", regex_matches(module, $module_pattern)"); + } + + #[test] + fn test_optional_condition_builder_with_value() { + let builder = OptionalConditionBuilder::new("arity", "arity"); + assert_eq!(builder.build(true), "arity == $arity"); + } + + #[test] + fn test_optional_condition_builder_without_value() { + let builder = OptionalConditionBuilder::new("arity", "arity"); + assert_eq!(builder.build(false), ""); + } + + #[test] + fn test_optional_condition_builder_with_default() { + let builder = OptionalConditionBuilder::new("arity", "arity").when_none("true"); + assert_eq!(builder.build(false), "true"); + } + + #[test] + fn test_optional_condition_builder_with_leading_comma() { + let builder = OptionalConditionBuilder::new("arity", "arity") + .with_leading_comma() + .when_none("true"); + assert_eq!(builder.build(true), ", arity == $arity"); + assert_eq!(builder.build(false), ", true"); + } +} diff --git a/src/test_utils.rs b/db/src/test_utils.rs similarity index 76% rename from src/test_utils.rs rename to db/src/test_utils.rs index 10da5b5..9ea9085 100644 --- a/src/test_utils.rs +++ b/db/src/test_utils.rs @@ -1,19 +1,22 @@ -//! Shared test utilities for execute and integration tests. +//! Shared test utilities for database and integration tests. //! -//! This module provides common helpers used across command execute tests. +//! This module provides common helpers for setting up test databases with fixture data. +#[cfg(feature = "test-utils")] use std::io::Write; use cozo::DbInstance; +#[cfg(feature = "test-utils")] use tempfile::NamedTempFile; +#[cfg(any(test, feature = "test-utils"))] use crate::queries::import::import_json_str; -use crate::commands::Execute; use crate::db::open_mem_db; /// Create a temporary file containing the given content. /// /// Used to create JSON files for importing test data. +#[cfg(feature = "test-utils")] pub fn create_temp_json_file(content: &str) -> NamedTempFile { let mut file = NamedTempFile::new().expect("Failed to create temp file"); file.write_all(content.as_bytes()) @@ -25,35 +28,33 @@ pub fn create_temp_json_file(content: &str) -> NamedTempFile { /// /// This is the standard setup for execute tests: create an in-memory DB, /// import test data, return the DB instance for command execution. +#[cfg(any(test, feature = "test-utils"))] pub fn setup_test_db(json_content: &str, project: &str) -> DbInstance { let db = open_mem_db(); import_json_str(&db, json_content, project).expect("Import should succeed"); db } -/// Execute a command against a database and return the result. -pub fn execute_cmd(cmd: C, db: &DbInstance) -> Result> { - cmd.execute(db) -} - -/// Execute a command against an empty (uninitialized) database. +/// Create an empty in-memory database. /// -/// Used to verify commands fail gracefully on empty DBs. -pub fn execute_on_empty_db(cmd: C) -> Result> { - let db = open_mem_db(); - cmd.execute(&db) +/// Used to verify queries fail gracefully on empty DBs. +#[cfg(any(test, feature = "test-utils"))] +pub fn setup_empty_test_db() -> DbInstance { + open_mem_db() } // ============================================================================= // Fixture-based helpers // ============================================================================= +#[cfg(any(test, feature = "test-utils"))] use crate::fixtures; /// Create a test database with call graph data. /// /// Use for: trace, reverse_trace, calls_from, calls_to, path, hotspots, /// unused, depends_on, depended_by +#[cfg(any(test, feature = "test-utils"))] pub fn call_graph_db(project: &str) -> DbInstance { setup_test_db(fixtures::CALL_GRAPH, project) } @@ -61,6 +62,7 @@ pub fn call_graph_db(project: &str) -> DbInstance { /// Create a test database with type signature data. /// /// Use for: search (functions kind), function +#[cfg(any(test, feature = "test-utils"))] pub fn type_signatures_db(project: &str) -> DbInstance { setup_test_db(fixtures::TYPE_SIGNATURES, project) } @@ -68,6 +70,7 @@ pub fn type_signatures_db(project: &str) -> DbInstance { /// Create a test database with struct definitions. /// /// Use for: struct command +#[cfg(any(test, feature = "test-utils"))] pub fn structs_db(project: &str) -> DbInstance { setup_test_db(fixtures::STRUCTS, project) } @@ -79,6 +82,7 @@ pub fn structs_db(project: &str) -> DbInstance { use std::path::Path; /// Load a fixture file from src/fixtures/output// +#[cfg(any(test, feature = "test-utils"))] pub fn load_output_fixture(command: &str, name: &str) -> String { let fixture_path = Path::new(env!("CARGO_MANIFEST_DIR")) .join("src/fixtures/output") diff --git a/src/types/call.rs b/db/src/types/call.rs similarity index 100% rename from src/types/call.rs rename to db/src/types/call.rs diff --git a/src/types/mod.rs b/db/src/types/mod.rs similarity index 100% rename from src/types/mod.rs rename to db/src/types/mod.rs diff --git a/src/types/results.rs b/db/src/types/results.rs similarity index 100% rename from src/types/results.rs rename to db/src/types/results.rs diff --git a/src/types/trace.rs b/db/src/types/trace.rs similarity index 100% rename from src/types/trace.rs rename to db/src/types/trace.rs diff --git a/src/commands/accepts/execute.rs b/src/commands/accepts/execute.rs deleted file mode 100644 index ece373c..0000000 --- a/src/commands/accepts/execute.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::error::Error; - -use serde::Serialize; - -use super::AcceptsCmd; -use crate::commands::Execute; -use crate::queries::accepts::{find_accepts, AcceptsEntry}; -use crate::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, -} - -impl ModuleGroupResult { - /// Build grouped result from flat AcceptsEntry list - fn from_entries( - pattern: String, - module_filter: Option, - entries: Vec, - ) -> Self { - 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; - - fn execute(self, db: &cozo::DbInstance) -> Result> { - let entries = find_accepts( - db, - &self.pattern, - &self.common.project, - self.common.regex, - self.module.as_deref(), - self.common.limit, - )?; - - Ok(>::from_entries( - self.pattern, - self.module, - entries, - )) - } -} diff --git a/src/commands/calls_from/execute.rs b/src/commands/calls_from/execute.rs deleted file mode 100644 index 873afb9..0000000 --- a/src/commands/calls_from/execute.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::error::Error; - -use serde::Serialize; - -use super::CallsFromCmd; -use crate::commands::Execute; -use crate::queries::calls_from::find_calls_from; -use crate::types::{Call, ModuleGroupResult}; -use crate::utils::group_calls; - -/// A caller function with all its outgoing calls -#[derive(Debug, Clone, Serialize)] -pub struct CallerFunction { - pub name: String, - pub arity: i64, - pub kind: String, - pub start_line: i64, - pub end_line: i64, - pub calls: Vec, -} - -impl ModuleGroupResult { - /// Build grouped result from flat calls - pub fn from_calls(module_pattern: String, function_pattern: String, calls: Vec) -> Self { - let (total_items, items) = group_calls( - calls, - // Group by caller module - |call| call.caller.module.to_string(), - // Key by caller function metadata - |call| CallerFunctionKey { - name: call.caller.name.to_string(), - arity: call.caller.arity, - kind: call.caller.kind.as_deref().unwrap_or("").to_string(), - start_line: call.caller.start_line.unwrap_or(0), - end_line: call.caller.end_line.unwrap_or(0), - }, - // Sort by line number - |a, b| a.line.cmp(&b.line), - // Deduplicate by callee (module, name, arity) - |c| (c.callee.module.to_string(), c.callee.name.to_string(), c.callee.arity), - // Build CallerFunction entry - |key, calls| CallerFunction { - name: key.name, - arity: key.arity, - kind: key.kind, - start_line: key.start_line, - end_line: key.end_line, - calls, - }, - // File tracking strategy: extract from first call in first function - |_module, functions_map| { - functions_map - .values() - .next() - .and_then(|calls| calls.first()) - .and_then(|call| call.caller.file.as_deref()) - .unwrap_or("") - .to_string() - }, - ); - - ModuleGroupResult { - module_pattern, - function_pattern: Some(function_pattern), - total_items, - items, - } - } -} - -/// Key for grouping by caller function (used internally) -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct CallerFunctionKey { - name: String, - arity: i64, - kind: String, - start_line: i64, - end_line: i64, -} - -impl Execute for CallsFromCmd { - type Output = ModuleGroupResult; - - fn execute(self, db: &cozo::DbInstance) -> Result> { - let calls = find_calls_from( - db, - &self.module, - self.function.as_deref(), - self.arity, - &self.common.project, - self.common.regex, - self.common.limit, - )?; - - Ok(>::from_calls( - self.module, - self.function.unwrap_or_default(), - calls, - )) - } -} diff --git a/src/commands/calls_from/output_tests.rs b/src/commands/calls_from/output_tests.rs deleted file mode 100644 index cd77a74..0000000 --- a/src/commands/calls_from/output_tests.rs +++ /dev/null @@ -1,162 +0,0 @@ -//! Output formatting tests for calls-from command. - -#[cfg(test)] -mod tests { - use super::super::execute::CallerFunction; - use crate::types::{Call, FunctionRef, ModuleGroupResult}; - use rstest::{fixture, rstest}; - - // ========================================================================= - // Expected outputs - // ========================================================================= - - const EMPTY_TABLE: &str = "\ -Calls from: MyApp.Accounts.get_user - -No calls found."; - - const SINGLE_TABLE: &str = "\ -Calls from: MyApp.Accounts.get_user - -Found 1 call(s): - -MyApp.Accounts (lib/my_app/accounts.ex) - get_user/1 (10:15) - → @ L12 MyApp.Repo.get/2"; - - const MULTIPLE_TABLE: &str = "\ -Calls from: MyApp.Accounts - -Found 2 call(s): - -MyApp.Accounts (lib/my_app/accounts.ex) - get_user/1 (10:15) - → @ L12 MyApp.Repo.get/2 - list_users/0 (20:25) - → @ L22 MyApp.Repo.all/1"; - - // ========================================================================= - // Fixtures - // ========================================================================= - - #[fixture] - fn empty_result() -> ModuleGroupResult { - >::from_calls( - "MyApp.Accounts".to_string(), - "get_user".to_string(), - vec![], - ) - } - - #[fixture] - fn single_result() -> ModuleGroupResult { - >::from_calls( - "MyApp.Accounts".to_string(), - "get_user".to_string(), - vec![Call { - caller: FunctionRef::with_definition( - "MyApp.Accounts", - "get_user", - 1, - "", - "lib/my_app/accounts.ex", - 10, - 15, - ), - callee: FunctionRef::new("MyApp.Repo", "get", 2), - line: 12, - call_type: Some("remote".to_string()), - depth: None, - }], - ) - } - - #[fixture] - fn multiple_result() -> ModuleGroupResult { - >::from_calls( - "MyApp.Accounts".to_string(), - String::new(), - vec![ - Call { - caller: FunctionRef::with_definition( - "MyApp.Accounts", - "get_user", - 1, - "", - "lib/my_app/accounts.ex", - 10, - 15, - ), - callee: FunctionRef::new("MyApp.Repo", "get", 2), - line: 12, - call_type: Some("remote".to_string()), - depth: None, - }, - Call { - caller: FunctionRef::with_definition( - "MyApp.Accounts", - "list_users", - 0, - "", - "lib/my_app/accounts.ex", - 20, - 25, - ), - callee: FunctionRef::new("MyApp.Repo", "all", 1), - line: 22, - call_type: Some("remote".to_string()), - depth: None, - }, - ], - ) - } - - // ========================================================================= - // Tests - // ========================================================================= - - crate::output_table_test! { - test_name: test_to_table_empty, - fixture: empty_result, - fixture_type: ModuleGroupResult, - expected: EMPTY_TABLE, - } - - crate::output_table_test! { - test_name: test_to_table_single, - fixture: single_result, - fixture_type: ModuleGroupResult, - expected: SINGLE_TABLE, - } - - crate::output_table_test! { - test_name: test_to_table_multiple, - fixture: multiple_result, - fixture_type: ModuleGroupResult, - expected: MULTIPLE_TABLE, - } - - crate::output_table_test! { - test_name: test_format_json, - fixture: single_result, - fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("calls_from", "single.json"), - format: Json, - } - - crate::output_table_test! { - test_name: test_format_toon, - fixture: single_result, - fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("calls_from", "single.toon"), - format: Toon, - } - - crate::output_table_test! { - test_name: test_format_toon_empty, - fixture: empty_result, - fixture_type: ModuleGroupResult, - expected: crate::test_utils::load_output_fixture("calls_from", "empty.toon"), - format: Toon, - } -} diff --git a/src/commands/calls_to/execute.rs b/src/commands/calls_to/execute.rs deleted file mode 100644 index 866d574..0000000 --- a/src/commands/calls_to/execute.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::error::Error; - -use serde::Serialize; - -use super::CallsToCmd; -use crate::commands::Execute; -use crate::queries::calls_to::find_calls_to; -use crate::types::{Call, ModuleGroupResult}; -use crate::utils::group_calls; - -/// A callee function (target) with all its callers -#[derive(Debug, Clone, Serialize)] -pub struct CalleeFunction { - pub name: String, - pub arity: i64, - pub callers: Vec, -} - -impl ModuleGroupResult { - /// Build grouped result from flat calls - pub fn from_calls(module_pattern: String, function_pattern: String, calls: Vec) -> Self { - let (total_items, items) = group_calls( - calls, - // Group by callee module - |call| call.callee.module.to_string(), - // Key by callee function metadata - |call| CalleeFunctionKey { - name: call.callee.name.to_string(), - arity: call.callee.arity, - }, - // Sort by caller module, name, arity, then line - |a, b| { - a.caller.module.as_ref().cmp(b.caller.module.as_ref()) - .then_with(|| a.caller.name.as_ref().cmp(b.caller.name.as_ref())) - .then_with(|| a.caller.arity.cmp(&b.caller.arity)) - .then_with(|| a.line.cmp(&b.line)) - }, - // Deduplicate by caller (module, name, arity) - |c| (c.caller.module.to_string(), c.caller.name.to_string(), c.caller.arity), - // Build CalleeFunction entry - |key, callers| CalleeFunction { - name: key.name, - arity: key.arity, - callers, - }, - // File is intentionally empty because callees are the grouping key, - // and a module can be defined across multiple files. The calls themselves - // carry file information where needed. - |_module, _map| String::new(), - ); - - ModuleGroupResult { - module_pattern, - function_pattern: Some(function_pattern), - total_items, - items, - } - } -} - -/// Key for grouping by callee function -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct CalleeFunctionKey { - name: String, - arity: i64, -} - -impl Execute for CallsToCmd { - type Output = ModuleGroupResult; - - fn execute(self, db: &cozo::DbInstance) -> Result> { - let calls = find_calls_to( - db, - &self.module, - self.function.as_deref(), - self.arity, - &self.common.project, - self.common.regex, - self.common.limit, - )?; - - Ok(>::from_calls( - self.module, - self.function.unwrap_or_default(), - calls, - )) - } -} diff --git a/src/commands/depended_by/execute.rs b/src/commands/depended_by/execute.rs deleted file mode 100644 index ccb45ab..0000000 --- a/src/commands/depended_by/execute.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::collections::BTreeMap; -use std::error::Error; - -use serde::Serialize; - -use super::DependedByCmd; -use crate::commands::Execute; -use crate::queries::depended_by::find_dependents; -use crate::types::{Call, ModuleGroupResult, ModuleGroup}; - -/// A target function being called in the dependency module -#[derive(Debug, Clone, Serialize)] -pub struct DependentTarget { - pub function: String, - pub arity: i64, - pub line: i64, -} - -/// A caller function in a dependent module -#[derive(Debug, Clone, Serialize)] -pub struct DependentCaller { - pub function: String, - pub arity: i64, - pub kind: String, - pub start_line: i64, - pub end_line: i64, - pub file: String, - pub targets: Vec, -} - -impl ModuleGroupResult { - /// Build a grouped structure from flat calls - pub fn from_calls(target_module: String, calls: Vec) -> Self { - let total_items = calls.len(); - - if calls.is_empty() { - return ModuleGroupResult { - module_pattern: target_module, - function_pattern: None, - total_items: 0, - items: vec![], - }; - } - - // Group by caller_module -> caller_function -> targets - // Using BTreeMap for automatic sorting by module and function key - let mut by_module: BTreeMap>> = BTreeMap::new(); - for call in &calls { - by_module - .entry(call.caller.module.to_string()) - .or_default() - .entry((call.caller.name.to_string(), call.caller.arity)) - .or_default() - .push(call); - } - - let items: Vec> = by_module - .into_iter() - .map(|(module_name, callers_map)| { - // Determine module file from first caller in first function - let module_file = callers_map - .values() - .next() - .and_then(|calls| calls.first()) - .and_then(|call| call.caller.file.as_deref()) - .unwrap_or("") - .to_string(); - - let entries: Vec = callers_map - .into_iter() - .map(|((func_name, arity), func_calls)| { - let first = func_calls[0]; - - let targets: Vec = func_calls - .iter() - .map(|c| DependentTarget { - function: c.callee.name.to_string(), - arity: c.callee.arity, - line: c.line, - }) - .collect(); - - DependentCaller { - function: func_name, - arity, - kind: first.caller.kind.as_deref().unwrap_or("").to_string(), - start_line: first.caller.start_line.unwrap_or(0), - end_line: first.caller.end_line.unwrap_or(0), - file: first.caller.file.as_deref().unwrap_or("").to_string(), - targets, - } - }) - .collect(); - - ModuleGroup { - name: module_name, - file: module_file, - entries, - function_count: None, - } - }) - .collect(); - - ModuleGroupResult { - module_pattern: target_module, - function_pattern: None, - total_items, - items, - } - } -} - -impl Execute for DependedByCmd { - type Output = ModuleGroupResult; - - fn execute(self, db: &cozo::DbInstance) -> Result> { - let calls = find_dependents( - db, - &self.module, - &self.common.project, - self.common.regex, - self.common.limit, - )?; - - Ok(>::from_calls(self.module, calls)) - } -} diff --git a/src/commands/depends_on/execute.rs b/src/commands/depends_on/execute.rs deleted file mode 100644 index a645581..0000000 --- a/src/commands/depends_on/execute.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::collections::BTreeMap; -use std::error::Error; - -use serde::Serialize; - -use super::DependsOnCmd; -use crate::commands::Execute; -use crate::queries::depends_on::find_dependencies; -use crate::types::{Call, ModuleGroupResult}; -use crate::utils::convert_to_module_groups; - -/// A function in a dependency module being called -#[derive(Debug, Clone, Serialize)] -pub struct DependencyFunction { - pub name: String, - pub arity: i64, - pub callers: Vec, -} - -impl ModuleGroupResult { - /// Build a grouped structure from flat calls - pub fn from_calls(source_module: String, calls: Vec) -> Self { - let total_items = calls.len(); - - if calls.is_empty() { - return ModuleGroupResult { - module_pattern: source_module, - function_pattern: None, - total_items: 0, - items: vec![], - }; - } - - // Group by callee_module -> callee_function -> callers - // Using BTreeMap for automatic sorting - let mut by_module: BTreeMap>> = BTreeMap::new(); - for call in calls { - by_module - .entry(call.callee.module.to_string()) - .or_default() - .entry((call.callee.name.to_string(), call.callee.arity)) - .or_default() - .push(call); - } - - // Convert to ModuleGroup structure - let items = convert_to_module_groups( - by_module, - |(func_name, arity), callers| DependencyFunction { - name: func_name, - arity, - callers, - }, - // File is intentionally empty because dependencies are the grouping key, - // and a module can depend on functions defined across multiple files. - // The dependency targets themselves carry file information where needed. - |_module, _map| String::new(), - ); - - ModuleGroupResult { - module_pattern: source_module, - function_pattern: None, - total_items, - items, - } - } -} - -impl Execute for DependsOnCmd { - type Output = ModuleGroupResult; - - fn execute(self, db: &cozo::DbInstance) -> Result> { - let calls = find_dependencies( - db, - &self.module, - &self.common.project, - self.common.regex, - self.common.limit, - )?; - - Ok(>::from_calls(self.module, calls)) - } -} diff --git a/src/commands/function/execute.rs b/src/commands/function/execute.rs deleted file mode 100644 index f14fdbb..0000000 --- a/src/commands/function/execute.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::error::Error; - -use serde::Serialize; - -use super::FunctionCmd; -use crate::commands::Execute; -use crate::queries::function::{find_functions, FunctionSignature}; -use crate::types::ModuleGroupResult; - -/// A function signature within a module -#[derive(Debug, Clone, Serialize)] -pub struct FuncSig { - pub name: String, - pub arity: i64, - #[serde(skip_serializing_if = "String::is_empty")] - pub args: String, - #[serde(skip_serializing_if = "String::is_empty")] - pub return_type: String, -} - -impl ModuleGroupResult { - /// Build grouped result from flat FunctionSignature list - fn from_signatures( - module_pattern: String, - function_pattern: String, - signatures: Vec, - ) -> Self { - let total_items = signatures.len(); - - // Use helper to group by module - let items = crate::utils::group_by_module(signatures, |sig| { - let func_sig = FuncSig { - name: sig.name, - arity: sig.arity, - args: sig.args, - return_type: sig.return_type, - }; - // File is intentionally empty for functions because the function command - // queries the functions table which doesn't track file locations. - // File locations are available in function_locations table if needed. - (sig.module, func_sig) - }); - - ModuleGroupResult { - module_pattern, - function_pattern: Some(function_pattern), - total_items, - items, - } - } -} - -impl Execute for FunctionCmd { - type Output = ModuleGroupResult; - - fn execute(self, db: &cozo::DbInstance) -> Result> { - let signatures = find_functions( - db, - &self.module, - &self.function, - self.arity, - &self.common.project, - self.common.regex, - self.common.limit, - )?; - - Ok(>::from_signatures( - self.module, - self.function, - signatures, - )) - } -} diff --git a/src/commands/returns/execute.rs b/src/commands/returns/execute.rs deleted file mode 100644 index 20eeeac..0000000 --- a/src/commands/returns/execute.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::error::Error; - -use serde::Serialize; - -use super::ReturnsCmd; -use crate::commands::Execute; -use crate::queries::returns::{find_returns, ReturnEntry}; -use crate::types::ModuleGroupResult; - -/// A function's return type information -#[derive(Debug, Clone, Serialize)] -pub struct ReturnInfo { - pub name: String, - pub arity: i64, - pub return_type: String, - pub line: i64, -} - -impl ModuleGroupResult { - /// Build grouped result from flat ReturnEntry list - fn from_entries( - pattern: String, - module_filter: Option, - entries: Vec, - ) -> Self { - let total_items = entries.len(); - - // Use helper to group by module - let items = crate::utils::group_by_module(entries, |entry| { - let return_info = ReturnInfo { - name: entry.name, - arity: entry.arity, - return_type: entry.return_string, - line: entry.line, - }; - (entry.module, return_info) - }); - - ModuleGroupResult { - module_pattern: module_filter.unwrap_or_else(|| "*".to_string()), - function_pattern: Some(pattern), - total_items, - items, - } - } -} - -impl Execute for ReturnsCmd { - type Output = ModuleGroupResult; - - fn execute(self, db: &cozo::DbInstance) -> Result> { - let entries = find_returns( - db, - &self.pattern, - &self.common.project, - self.common.regex, - self.module.as_deref(), - self.common.limit, - )?; - - Ok(>::from_entries( - self.pattern, - self.module, - entries, - )) - } -} diff --git a/src/commands/reverse_trace/execute.rs b/src/commands/reverse_trace/execute.rs deleted file mode 100644 index ecb9518..0000000 --- a/src/commands/reverse_trace/execute.rs +++ /dev/null @@ -1,157 +0,0 @@ -use std::collections::HashMap; -use std::error::Error; - -use super::ReverseTraceCmd; -use crate::commands::Execute; -use crate::queries::reverse_trace::{reverse_trace_calls, ReverseTraceStep}; -use crate::types::{TraceDirection, TraceEntry, TraceResult}; - -impl TraceResult { - /// Build a flattened reverse-trace from ReverseTraceStep objects - pub fn from_steps( - target_module: String, - target_function: String, - max_depth: u32, - steps: Vec, - ) -> Self { - let mut entries = Vec::new(); - let mut entry_index_map: HashMap<(String, String, i64, i64), usize> = HashMap::new(); - - if steps.is_empty() { - return Self::empty(target_module, target_function, max_depth, TraceDirection::Backward); - } - - // Group steps by depth - let mut by_depth: HashMap> = HashMap::new(); - for step in &steps { - by_depth.entry(step.depth).or_default().push(step); - } - - // Process depth 1 (direct callers of target function) - if let Some(depth1_steps) = by_depth.get(&1) { - let mut filter = crate::dedup::DeduplicationFilter::new(); - - for step in depth1_steps { - let caller_key = ( - step.caller_module.clone(), - step.caller_function.clone(), - step.caller_arity, - 1i64, - ); - - // Add caller as root entry if not already added - if filter.should_process(caller_key.clone()) { - let entry_idx = entries.len(); - entries.push(TraceEntry { - module: step.caller_module.clone(), - function: step.caller_function.clone(), - arity: step.caller_arity, - kind: step.caller_kind.clone(), - start_line: step.caller_start_line, - end_line: step.caller_end_line, - file: step.file.clone(), - depth: 1, - line: step.line, - parent_index: None, - }); - entry_index_map.insert(caller_key, entry_idx); - } - } - } - - // Process deeper levels (additional callers) - for depth in 2..=max_depth as i64 { - if let Some(depth_steps) = by_depth.get(&depth) { - let mut filter = crate::dedup::DeduplicationFilter::new(); - - for step in depth_steps { - let caller_key = ( - step.caller_module.clone(), - step.caller_function.clone(), - step.caller_arity, - depth, - ); - - // Find parent index (the callee at previous depth, which is what called this caller) - let parent_key = ( - step.callee_module.clone(), - step.callee_function.clone(), - step.callee_arity, - depth - 1, - ); - - let parent_index = entry_index_map.get(&parent_key).copied(); - - if filter.should_process(caller_key.clone()) && parent_index.is_some() { - let entry_idx = entries.len(); - entries.push(TraceEntry { - module: step.caller_module.clone(), - function: step.caller_function.clone(), - arity: step.caller_arity, - kind: step.caller_kind.clone(), - start_line: step.caller_start_line, - end_line: step.caller_end_line, - file: step.file.clone(), - depth, - line: step.line, - parent_index, - }); - entry_index_map.insert(caller_key, entry_idx); - } - } - } - } - - let total_items = entries.len(); - - Self { - module: target_module, - function: target_function, - max_depth, - direction: TraceDirection::Backward, - total_items, - entries, - } - } -} - -impl Execute for ReverseTraceCmd { - type Output = TraceResult; - - fn execute(self, db: &cozo::DbInstance) -> Result> { - let steps = reverse_trace_calls( - db, - &self.module, - &self.function, - self.arity, - &self.common.project, - self.common.regex, - self.depth, - self.common.limit, - )?; - - Ok(TraceResult::from_steps( - self.module, - self.function, - self.depth, - steps, - )) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_empty_reverse_trace() { - let result = TraceResult::from_steps( - "TestModule".to_string(), - "test_func".to_string(), - 5, - vec![], - ); - assert_eq!(result.total_items, 0); - assert_eq!(result.entries.len(), 0); - } -} diff --git a/src/commands/struct_usage/execute.rs b/src/commands/struct_usage/execute.rs deleted file mode 100644 index 6175720..0000000 --- a/src/commands/struct_usage/execute.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::collections::{BTreeMap, HashSet}; -use std::error::Error; - -use serde::Serialize; - -use super::StructUsageCmd; -use crate::commands::Execute; -use crate::queries::struct_usage::{find_struct_usage, StructUsageEntry}; -use crate::types::ModuleGroupResult; - -/// A function that uses a struct type -#[derive(Debug, Clone, Serialize)] -pub struct UsageInfo { - pub name: String, - pub arity: i64, - pub inputs: String, - pub returns: String, - pub line: i64, -} - -/// A module and its usage counts for a struct type -#[derive(Debug, Clone, Serialize)] -pub struct ModuleStructUsage { - pub name: String, - pub accepts_count: i64, - pub returns_count: i64, - pub total: i64, -} - -/// Result containing aggregated module-level struct usage -#[derive(Debug, Clone, Serialize)] -pub struct StructModulesResult { - pub struct_pattern: String, - pub total_modules: usize, - pub total_functions: usize, - pub modules: Vec, -} - -/// Output type that can be either detailed or aggregated -#[derive(Debug, Serialize)] -#[serde(untagged)] -pub enum StructUsageOutput { - Detailed(ModuleGroupResult), - ByModule(StructModulesResult), -} - -impl ModuleGroupResult { - /// Build grouped result from flat StructUsageEntry list - fn from_entries( - pattern: String, - module_filter: Option, - entries: Vec, - ) -> Self { - let total_items = entries.len(); - - // Use helper to group by module - let items = crate::utils::group_by_module(entries, |entry| { - let usage_info = UsageInfo { - name: entry.name, - arity: entry.arity, - inputs: entry.inputs_string, - returns: entry.return_string, - line: entry.line, - }; - (entry.module, usage_info) - }); - - ModuleGroupResult { - module_pattern: module_filter.unwrap_or_else(|| "*".to_string()), - function_pattern: Some(pattern), - total_items, - items, - } - } -} - -impl StructModulesResult { - /// Build aggregated result from flat StructUsageEntry list - fn from_entries(pattern: String, entries: Vec) -> Self { - // Aggregate by module, tracking which functions accept vs return - let mut module_map: BTreeMap> = BTreeMap::new(); - let mut module_accepts: BTreeMap> = BTreeMap::new(); - let mut module_returns: BTreeMap> = BTreeMap::new(); - - for entry in &entries { - // Track unique functions per module - module_map - .entry(entry.module.clone()) - .or_default() - .insert(format!("{}/{}", entry.name, entry.arity)); - - // Check if function accepts the type - if entry.inputs_string.contains(&pattern) { - module_accepts - .entry(entry.module.clone()) - .or_default() - .insert(format!("{}/{}", entry.name, entry.arity)); - } - - // Check if function returns the type - if entry.return_string.contains(&pattern) { - module_returns - .entry(entry.module.clone()) - .or_default() - .insert(format!("{}/{}", entry.name, entry.arity)); - } - } - - // Convert to result type, sorted by total count descending - let mut modules: Vec = module_map - .into_iter() - .map(|(name, functions)| { - let accepts_count = module_accepts - .get(&name) - .map(|s| s.len() as i64) - .unwrap_or(0); - let returns_count = module_returns - .get(&name) - .map(|s| s.len() as i64) - .unwrap_or(0); - let total = functions.len() as i64; - - ModuleStructUsage { - name, - accepts_count, - returns_count, - total, - } - }) - .collect(); - - // Sort by total count descending, then by module name - modules.sort_by(|a, b| { - let cmp = b.total.cmp(&a.total); - if cmp == std::cmp::Ordering::Equal { - a.name.cmp(&b.name) - } else { - cmp - } - }); - - let total_modules = modules.len(); - let total_functions = entries.len(); - - StructModulesResult { - struct_pattern: pattern, - total_modules, - total_functions, - modules, - } - } -} - -impl Execute for StructUsageCmd { - type Output = StructUsageOutput; - - fn execute(self, db: &cozo::DbInstance) -> Result> { - let entries = find_struct_usage( - db, - &self.pattern, - &self.common.project, - self.common.regex, - self.module.as_deref(), - self.common.limit, - )?; - - if self.by_module { - Ok(StructUsageOutput::ByModule( - StructModulesResult::from_entries(self.pattern, entries), - )) - } else { - Ok(StructUsageOutput::Detailed( - ModuleGroupResult::::from_entries(self.pattern, self.module, entries), - )) - } - } -} diff --git a/src/commands/trace/execute.rs b/src/commands/trace/execute.rs deleted file mode 100644 index 7d560bb..0000000 --- a/src/commands/trace/execute.rs +++ /dev/null @@ -1,179 +0,0 @@ -use std::collections::HashMap; -use std::error::Error; - -use super::TraceCmd; -use crate::commands::Execute; -use crate::queries::trace::trace_calls; -use crate::types::{Call, TraceDirection, TraceEntry, TraceResult}; - -impl TraceResult { - /// Build a flattened trace from Call objects - pub fn from_calls( - start_module: String, - start_function: String, - max_depth: u32, - calls: Vec, - ) -> Self { - let mut entries = Vec::new(); - let mut entry_index_map: HashMap<(String, String, i64, i64), usize> = HashMap::new(); - - // Add the starting function as the root entry at depth 0 - entries.push(TraceEntry { - module: start_module.clone(), - function: start_function.clone(), - arity: 0, // Will be updated from first call if available - kind: String::new(), - start_line: 0, - end_line: 0, - file: String::new(), - depth: 0, - line: 0, - parent_index: None, - }); - entry_index_map.insert((start_module.clone(), start_function.clone(), 0, 0), 0); - - if calls.is_empty() { - return Self::empty(start_module, start_function, max_depth, TraceDirection::Forward); - } - - // Group calls by depth, consuming the Vec to take ownership - let mut by_depth: HashMap> = HashMap::new(); - for call in calls { - if let Some(depth) = call.depth { - by_depth.entry(depth).or_default().push(call); - } - } - - // Process depth 1 (direct callees from start function) - if let Some(depth1_calls) = by_depth.remove(&1) { - // Track seen entries by index into entries vec (avoids storing strings) - let mut seen_at_depth: std::collections::HashSet = std::collections::HashSet::new(); - - for call in depth1_calls { - // Check if we already have this callee at this depth - let existing = entries.iter().position(|e| { - e.depth == 1 - && e.module == call.callee.module.as_ref() - && e.function == call.callee.name.as_ref() - && e.arity == call.callee.arity - }); - - if existing.is_none() || seen_at_depth.insert(existing.unwrap_or(usize::MAX)) { - if existing.is_none() { - let entry_idx = entries.len(); - // Convert from Rc to String for storage - let module = call.callee.module.to_string(); - let function = call.callee.name.to_string(); - let arity = call.callee.arity; - entry_index_map.insert((module.clone(), function.clone(), arity, 1i64), entry_idx); - entries.push(TraceEntry { - module, - function, - arity, - kind: call.callee.kind.as_deref().unwrap_or("").to_string(), - start_line: call.callee.start_line.unwrap_or(0), - end_line: call.callee.end_line.unwrap_or(0), - file: call.callee.file.as_deref().unwrap_or("").to_string(), - depth: 1, - line: call.line, - parent_index: Some(0), - }); - } - } - } - } - - // Process deeper levels - for depth in 2..=max_depth as i64 { - if let Some(depth_calls) = by_depth.remove(&depth) { - for call in depth_calls { - // Check if we already have this callee at this depth - let existing = entries.iter().position(|e| { - e.depth == depth - && e.module == call.callee.module.as_ref() - && e.function == call.callee.name.as_ref() - && e.arity == call.callee.arity - }); - - if existing.is_none() { - // Find parent index using references (no cloning) - let parent_index = entries.iter().position(|e| { - e.depth == depth - 1 - && e.module == call.caller.module.as_ref() - && e.function == call.caller.name.as_ref() - && e.arity == call.caller.arity - }); - - if parent_index.is_some() { - let entry_idx = entries.len(); - // Convert from Rc to String for storage - let module = call.callee.module.to_string(); - let function = call.callee.name.to_string(); - let arity = call.callee.arity; - entry_index_map.insert((module.clone(), function.clone(), arity, depth), entry_idx); - entries.push(TraceEntry { - module, - function, - arity, - kind: call.callee.kind.as_deref().unwrap_or("").to_string(), - start_line: call.callee.start_line.unwrap_or(0), - end_line: call.callee.end_line.unwrap_or(0), - file: call.callee.file.as_deref().unwrap_or("").to_string(), - depth, - line: call.line, - parent_index, - }); - } - } - } - } - } - - let total_items = entries.len() - 1; // Exclude the root entry from count - - Self { - module: start_module, - function: start_function, - max_depth, - direction: TraceDirection::Forward, - total_items, - entries, - } - } -} - -impl Execute for TraceCmd { - type Output = TraceResult; - - fn execute(self, db: &cozo::DbInstance) -> Result> { - let calls = trace_calls( - db, - &self.module, - &self.function, - self.arity, - &self.common.project, - self.common.regex, - self.depth, - self.common.limit, - )?; - - Ok(TraceResult::from_calls( - self.module, - self.function, - self.depth, - calls, - )) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_empty_trace() { - let result = TraceResult::from_calls("TestModule".to_string(), "test_func".to_string(), 5, vec![]); - assert_eq!(result.total_items, 0); - assert_eq!(result.entries.len(), 0); - } -} diff --git a/src/commands/unused/execute.rs b/src/commands/unused/execute.rs deleted file mode 100644 index 77e86b2..0000000 --- a/src/commands/unused/execute.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::error::Error; - -use serde::Serialize; - -use super::UnusedCmd; -use crate::commands::Execute; -use crate::queries::unused::{find_unused_functions, UnusedFunction}; -use crate::types::ModuleCollectionResult; - -/// An unused function within a module -#[derive(Debug, Clone, Serialize)] -pub struct UnusedFunc { - pub name: String, - pub arity: i64, - pub kind: String, - pub line: i64, -} - -impl ModuleCollectionResult { - /// Build grouped result from flat UnusedFunction list - fn from_functions( - module_pattern: String, - functions: Vec, - ) -> Self { - let total_items = functions.len(); - - // Use helper to group by module, tracking file for each module - let items = crate::utils::group_by_module_with_file(functions, |func| { - let unused_func = UnusedFunc { - name: func.name, - arity: func.arity, - kind: func.kind, - line: func.line, - }; - (func.module, unused_func, func.file) - }); - - ModuleCollectionResult { - module_pattern, - function_pattern: None, - kind_filter: None, - name_filter: None, - total_items, - items, - } - } -} - -impl Execute for UnusedCmd { - type Output = ModuleCollectionResult; - - fn execute(self, db: &cozo::DbInstance) -> Result> { - let functions = find_unused_functions( - db, - self.module.as_deref(), - &self.common.project, - self.common.regex, - self.private_only, - self.public_only, - self.exclude_generated, - self.common.limit, - )?; - - Ok(>::from_functions( - self.module.unwrap_or_else(|| "*".to_string()), - functions, - )) - } -} -