#![feature(portable_simd)]
use tikv_jemallocator::Jemalloc;
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand, ValueEnum};
use sha2::{Digest, Sha256};
use std::io::{BufRead, BufReader, Write};
#[cfg(unix)]
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tracing_subscriber::EnvFilter;
use which::which;
mod ast;
mod callgraph;
mod cfg;
mod dfg;
mod diagnostics;
mod error;
mod lang;
mod metrics;
mod security;
mod simd;
mod util;
use util::path::{
detect_project_language, get_project_root,
require_directory, require_exists, require_file,
};
#[derive(Parser)]
#[command(
name = "brrr",
version,
author,
about = "Token-efficient code analysis for LLMs",
long_about = r#"
Token-efficient code analysis for LLMs.
Examples:
brrr tree src/ # File tree for src/
brrr structure . --lang python # Code structure for Python files
brrr extract src/main.py # Full file analysis
brrr context main --project . # LLM context starting from main()
brrr cfg src/main.py process # Control flow for process()
brrr slice src/main.py func 42 # Lines affecting line 42
Ignore Patterns:
brrr respects .brrrignore files (gitignore syntax).
Use --no-ignore to bypass ignore patterns.
Semantic Search:
First run: brrr semantic index . (downloads embedding model)
Then: brrr semantic search "authentication logic" .
"#
)]
struct Cli {
#[arg(long, global = true)]
no_ignore: bool,
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
verbose: u8,
#[arg(long, global = true, value_enum, default_value = "json")]
format: OutputFormat,
#[command(subcommand)]
command: Commands,
}
#[derive(Clone, Copy, ValueEnum, Default)]
enum OutputFormat {
#[default]
Json,
Text,
Mermaid,
Dot,
Csv,
}
#[derive(Clone, Copy, ValueEnum)]
enum SliceDirection {
Backward,
Forward,
}
impl Default for SliceDirection {
fn default() -> Self {
Self::Backward
}
}
#[derive(Clone, Copy, ValueEnum, Default, Debug)]
enum CouplingLevelArg {
#[default]
File,
Module,
Class,
}
impl From<CouplingLevelArg> for metrics::CouplingLevel {
fn from(arg: CouplingLevelArg) -> Self {
match arg {
CouplingLevelArg::File => Self::File,
CouplingLevelArg::Module => Self::Module,
CouplingLevelArg::Class => Self::Class,
}
}
}
#[derive(Clone, Copy, ValueEnum, Default, Debug)]
enum CircularLevelArg {
Package,
#[default]
Module,
Class,
Function,
}
impl From<CircularLevelArg> for quality::circular::DependencyLevel {
fn from(arg: CircularLevelArg) -> Self {
match arg {
CircularLevelArg::Package => Self::Package,
CircularLevelArg::Module => Self::Module,
CircularLevelArg::Class => Self::Class,
CircularLevelArg::Function => Self::Function,
}
}
}
#[derive(Clone, Copy, ValueEnum, Debug)]
enum Language {
Python,
Typescript,
Javascript,
Go,
Rust,
Java,
C,
Cpp,
Ruby,
Php,
Kotlin,
Swift,
Csharp,
Scala,
Lua,
Elixir,
}
impl std::fmt::Display for Language {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Language::Python => "python",
Language::Typescript => "typescript",
Language::Javascript => "javascript",
Language::Go => "go",
Language::Rust => "rust",
Language::Java => "java",
Language::C => "c",
Language::Cpp => "cpp",
Language::Ruby => "ruby",
Language::Php => "php",
Language::Kotlin => "kotlin",
Language::Swift => "swift",
Language::Csharp => "csharp",
Language::Scala => "scala",
Language::Lua => "lua",
Language::Elixir => "elixir",
};
write!(f, "{}", s)
}
}
#[derive(Clone, Copy, ValueEnum, Debug)]
enum WarmLanguage {
Python,
Typescript,
Javascript,
Go,
Rust,
Java,
C,
Cpp,
All,
}
impl Default for WarmLanguage {
fn default() -> Self {
Self::Python
}
}
impl std::fmt::Display for WarmLanguage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
WarmLanguage::Python => "python",
WarmLanguage::Typescript => "typescript",
WarmLanguage::Javascript => "javascript",
WarmLanguage::Go => "go",
WarmLanguage::Rust => "rust",
WarmLanguage::Java => "java",
WarmLanguage::C => "c",
WarmLanguage::Cpp => "cpp",
WarmLanguage::All => "all",
};
write!(f, "{}", s)
}
}
#[derive(Clone, Copy, ValueEnum, Default)]
enum SemanticBackend {
#[default]
Auto,
Tei,
SentenceTransformers,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
enum SearchTask {
#[default]
#[value(name = "code_search", alias = "code-search")]
CodeSearch,
#[value(name = "code_retrieval", alias = "code-retrieval")]
CodeRetrieval,
#[value(name = "semantic_search", alias = "semantic-search")]
SemanticSearch,
#[value(name = "default")]
Default,
}
impl SearchTask {
pub fn as_task_string(&self) -> &'static str {
match self {
Self::CodeSearch => "code_search",
Self::CodeRetrieval => "code_retrieval",
Self::SemanticSearch => "semantic_search",
Self::Default => "default",
}
}
}
#[derive(Subcommand)]
enum Commands {
#[command(
about = "Show file tree",
long_about = "Display the file tree structure of a directory in JSON format."
)]
Tree {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_name = "EXT", num_args = 1..)]
ext: Vec<String>,
#[arg(long)]
show_hidden: bool,
#[arg(long, value_name = "DEPTH")]
max_depth: Option<usize>,
},
#[command(
about = "Show code structure (codemaps)",
long_about = "Extract functions, classes, and methods from source files."
)]
Structure {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, visible_alias = "max", default_value = "50", value_name = "N")]
limit: usize,
},
#[command(
about = "Search files for pattern",
long_about = "Search files for a regex pattern with optional context lines."
)]
Search {
pattern: String,
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_name = "EXT", num_args = 1..)]
ext: Vec<String>,
#[arg(short = 'C', long, default_value = "0")]
context: usize,
#[arg(long, default_value = "100", value_name = "N")]
max: usize,
#[arg(long, default_value = "10000")]
max_files: usize,
},
#[command(
about = "Extract full file info",
long_about = "Extract complete AST info from a file: functions, classes, methods, docstrings."
)]
Extract {
file: PathBuf,
#[arg(long = "base-path", short = 'b', value_name = "DIR")]
base_path: Option<PathBuf>,
#[arg(long = "class", value_name = "NAME")]
filter_class: Option<String>,
#[arg(long = "function", value_name = "NAME")]
filter_function: Option<String>,
#[arg(long = "method", value_name = "CLASS.METHOD")]
filter_method: Option<String>,
},
#[command(
about = "Get relevant context for LLM",
long_about = "Build LLM-ready context by following the call graph from an entry point."
)]
Context {
entry: String,
#[arg(long, default_value = ".")]
project: PathBuf,
#[arg(long, default_value = "2")]
depth: usize,
#[arg(long, value_enum)]
lang: Option<Language>,
},
#[command(
about = "Control flow graph",
long_about = "Generate a control flow graph (CFG) for a function, showing branches and loops."
)]
Cfg {
file: PathBuf,
function: String,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
},
#[command(
about = "Data flow graph",
long_about = "Generate a data flow graph (DFG) for a function, showing variable dependencies."
)]
Dfg {
file: PathBuf,
function: String,
#[arg(long, value_enum)]
lang: Option<Language>,
},
#[command(
about = "Program slice",
long_about = "Compute a program slice: find all lines that affect (backward) or are affected by (forward) a given line."
)]
Slice {
file: PathBuf,
function: String,
line: usize,
#[arg(long, value_enum, default_value = "backward")]
direction: SliceDirection,
#[arg(long, value_name = "VAR")]
var: Option<String>,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, default_value = "false")]
extended: bool,
},
#[command(
about = "Build cross-file call graph",
long_about = "Build a project-wide call graph showing which functions call which."
)]
Calls {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long)]
extended: bool,
},
#[command(
about = "Find all callers of a function (reverse call graph)",
long_about = "Analyze impact: find all functions that call a given function (transitively).\nUseful before refactoring to understand what will be affected."
)]
Impact {
func: String,
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, default_value = "3")]
depth: usize,
#[arg(long)]
file: Option<String>,
#[arg(long, value_enum)]
lang: Option<Language>,
},
#[command(
about = "Find unreachable (dead) code",
long_about = "Find functions that are never called (dead code).\nExcludes common entry points like main, test_*, cli, etc."
)]
Dead {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_name = "PATTERN", num_args = 0..)]
entry: Vec<String>,
#[arg(long, value_enum)]
lang: Option<Language>,
},
#[command(
about = "Detect architectural layers from call patterns",
long_about = "Detect architectural layers (entry, middle, leaf) from call patterns.\nIdentifies circular dependencies."
)]
Arch {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
},
#[command(
about = "Parse imports from a source file",
long_about = "Parse all import statements from a source file.\nReturns JSON with module names, imported names, and aliases."
)]
Imports {
file: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
},
#[command(
about = "Find all files that import a module (reverse import lookup)",
long_about = "Find all files that import a given module.\nComplements 'brrr impact' which tracks function calls."
)]
Importers {
module: String,
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
},
#[command(
about = "Pre-build call graph cache for faster queries",
long_about = "Pre-build the call graph cache to speed up subsequent queries.\nRun this once per project before using impact/dead/calls."
)]
Warm {
path: PathBuf,
#[arg(long)]
background: bool,
#[arg(long, value_enum, default_value = "python")]
lang: WarmLanguage,
},
#[command(
about = "Find tests affected by changed files",
long_about = "Find which tests to run based on changed files.\nUses call graph + import analysis to find affected tests."
)]
ChangeImpact {
#[arg(value_name = "FILE")]
files: Vec<PathBuf>,
#[arg(long)]
session: bool,
#[arg(long)]
git: bool,
#[arg(long, default_value = "HEAD~1")]
git_base: String,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, default_value = "5")]
depth: usize,
#[arg(long)]
run: bool,
#[arg(short, long, default_value = ".")]
project: PathBuf,
},
#[command(
about = "Get type and lint diagnostics",
long_about = "Run type checker (pyright) and linter (ruff) on code.\nReturns structured errors. Use before tests to catch type errors early."
)]
Diagnostics {
target: PathBuf,
#[arg(long)]
project: bool,
#[arg(long)]
no_lint: bool,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, value_enum)]
lang: Option<Language>,
},
#[command(subcommand)]
Semantic(SemanticCommands),
#[command(subcommand)]
Daemon(DaemonCommands),
#[command(
about = "Check and install diagnostic tools (type checkers, linters)",
long_about = "Check which diagnostic tools (type checkers, linters) are installed.\nCan auto-install missing tools for supported languages."
)]
Doctor {
#[arg(long, value_name = "LANG")]
install: Option<String>,
#[arg(long)]
json: bool,
},
#[command(subcommand)]
Security(SecurityCommands),
#[command(subcommand_required = false, args_conflicts_with_subcommands = true)]
Metrics {
#[arg(default_value = ".")]
path: Option<PathBuf>,
#[arg(long, short = 'l', value_enum)]
lang: Option<Language>,
#[arg(long, short = 'f', value_enum, default_value = "text")]
format: Option<OutputFormat>,
#[arg(long)]
fail_on: Option<String>,
#[command(subcommand)]
cmd: Option<MetricsCommands>,
},
#[command(subcommand)]
Quality(QualityCommands),
}
#[derive(Subcommand)]
enum MetricsCommands {
#[command(
name = "complexity",
about = "Calculate cyclomatic complexity for functions",
long_about = "Calculate cyclomatic complexity using control flow graph analysis.\n\n\
Cyclomatic complexity measures the number of linearly independent paths\n\
through a function. Higher values indicate more complex code.\n\n\
Risk Levels:\n\
- Low (1-10): Simple, low risk, easy to test\n\
- Medium (11-20): Moderate complexity, consider refactoring\n\
- High (21-50): Complex, hard to test and maintain\n\
- Critical (50+): Refactor immediately\n\n\
Decision points counted:\n\
- if, elif/else if, while, for, switch/match cases\n\
- try/except/catch handlers\n\
- Boolean operators (&&, ||, and, or) in conditions"
)]
Complexity {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, short = 't', value_name = "N")]
threshold: Option<u32>,
#[arg(long, short = 's')]
sort: bool,
#[arg(long)]
violations_only: bool,
},
#[command(
name = "cognitive",
about = "Calculate cognitive complexity for functions",
long_about = "Calculate cognitive complexity using SonarSource methodology.\n\n\
Cognitive complexity measures how difficult code is to understand,\n\
penalizing deeply nested structures more than cyclomatic complexity.\n\n\
Key differences from cyclomatic complexity:\n\
- Nesting penalty: nested structures add to the increment\n\
- Logical sequences: a && b && c counts as 1, not 2\n\
- No increment for else (counted with if)\n\n\
Risk Levels (stricter than cyclomatic):\n\
- Low (0-5): Simple, easy to understand\n\
- Medium (6-10): Moderate complexity, consider simplifying\n\
- High (11-15): Complex, hard to understand\n\
- Critical (15+): Refactor immediately"
)]
Cognitive {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, short = 't', value_name = "N")]
threshold: Option<u32>,
#[arg(long, short = 's')]
sort: bool,
#[arg(long)]
violations_only: bool,
#[arg(long, short = 'b')]
breakdown: bool,
},
#[command(
name = "halstead",
about = "Calculate Halstead complexity metrics for functions",
long_about = "Calculate Halstead metrics based on operator and operand counts.\n\n\
Halstead metrics measure software complexity through vocabulary analysis:\n\n\
Base Counts:\n\
- n1: Number of distinct operators\n\
- n2: Number of distinct operands\n\
- N1: Total operators\n\
- N2: Total operands\n\n\
Derived Metrics:\n\
- Vocabulary (n): n1 + n2\n\
- Length (N): N1 + N2\n\
- Volume (V): N * log2(n) - program size in bits\n\
- Difficulty (D): (n1/2) * (N2/n2) - error-proneness\n\
- Effort (E): D * V - mental effort required\n\
- Time (T): E / 18 - estimated development time (seconds)\n\
- Bugs (B): V / 3000 - estimated bug count\n\n\
Language-specific operators are recognized for Python, TypeScript,\n\
JavaScript, Rust, Go, Java, C, and C++."
)]
Halstead {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, short = 's')]
sort: bool,
#[arg(long)]
sort_by_difficulty: bool,
#[arg(long)]
show_tokens: bool,
},
#[command(
name = "maintainability",
about = "Calculate Maintainability Index for functions",
long_about = "Calculate Maintainability Index combining Halstead Volume, Cyclomatic Complexity, and Lines of Code.\n\n\
Formula (Visual Studio Standard):\n\
MI = MAX(0, (171 - 5.2*ln(V) - 0.23*CC - 16.2*ln(LOC)) * 100/171)\n\n\
Where:\n\
- V = Halstead Volume (program size in bits)\n\
- CC = Cyclomatic Complexity (independent paths)\n\
- LOC = Source Lines of Code (excluding blanks/comments)\n\n\
Extended formula adds comment bonus:\n\
MI += 50 * sin(sqrt(2.4 * CM)) where CM = comment percentage\n\n\
Risk Levels:\n\
- Low (50-100): Highly maintainable\n\
- Medium (20-49): Moderately maintainable\n\
- High (10-19): Hard to maintain\n\
- Critical (0-9): Very hard to maintain, refactor immediately\n\n\
Lines of Code types calculated:\n\
- Physical LOC: Total lines\n\
- Source LOC: Non-blank lines\n\
- Effective LOC: Source minus comment-only lines\n\
- Comment lines: Lines containing comments"
)]
Maintainability {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, short = 't', value_name = "N")]
threshold: Option<f64>,
#[arg(long, short = 's')]
sort: bool,
#[arg(long)]
violations_only: bool,
#[arg(long)]
include_comments: bool,
},
#[command(
name = "loc",
about = "Calculate lines of code metrics",
long_about = "Calculate various lines of code metrics using AST-aware analysis.\n\n\
Metric Types:\n\
- Physical LOC: Total lines including blanks and comments\n\
- Source LOC (SLOC): Lines containing actual code\n\
- Logical LOC: Number of statements (AST-based)\n\
- Comment LOC (CLOC): Lines containing comments\n\
- Blank lines: Empty or whitespace-only lines\n\n\
AST-Aware Features:\n\
- Multi-line strings not counted as code lines (except docstrings)\n\
- Accurate statement counting from AST\n\
- Proper handling of mixed code-and-comment lines\n\n\
Per-function metrics:\n\
- Function SLOC with 'too long' detection (>50 lines threshold)\n\
- Statement count per function\n\
- Comment density percentage\n\n\
Use --by-language to see metrics grouped by programming language."
)]
Loc {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long)]
by_language: bool,
#[arg(long, short = 's')]
sort: bool,
#[arg(long, short = 't', default_value = "50")]
function_threshold: u32,
#[arg(long)]
violations_only: bool,
#[arg(long, default_value = "10")]
top: usize,
},
#[command(
name = "nesting",
about = "Calculate nesting depth metrics for functions",
long_about = "Calculate nesting depth metrics to identify deeply nested code.\n\n\
Deep nesting is a strong indicator of complexity that makes code harder\n\
to understand, test, and maintain.\n\n\
Nesting Constructs Tracked:\n\
- Control flow: if, for, while, switch/match, try\n\
- Closures/lambdas: lambda, arrow functions, closures\n\
- Callbacks: Nested function expressions (especially in JS)\n\
- Comprehensions: List/dict/set comprehensions with conditions (Python)\n\
- Blocks: Named blocks in Rust, defer in Go\n\n\
Risk Levels:\n\
- Good (1-3): Acceptable nesting, code is readable\n\
- Acceptable (4-5): Consider simplifying if possible\n\
- Complex (6-7): Hard to understand, should refactor\n\
- Severe (8+): Refactor immediately\n\n\
Suggestions for improvement are provided for deeply nested code."
)]
Nesting {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, short = 't', value_name = "N")]
threshold: Option<u32>,
#[arg(long, short = 's')]
sort: bool,
#[arg(long)]
violations_only: bool,
#[arg(long, short = 'd')]
details: bool,
},
#[command(
name = "functions",
about = "Calculate function size metrics",
long_about = "Calculate comprehensive size metrics for functions to identify oversized code.\n\n\
Metrics Calculated:\n\
- SLOC: Source Lines of Code (excluding blanks/comments)\n\
- Statements: Number of AST statement nodes\n\
- Parameters: Number of function parameters\n\
- Local Variables: Number of variable declarations\n\
- Return Statements: Number of return/throw points\n\
- Branches: Number of if/switch/match constructs\n\n\
Default Thresholds:\n\
- SLOC: warning > 30, critical > 60\n\
- Parameters: warning > 5, critical > 8\n\
- Variables: warning > 10, critical > 15\n\
- Returns: warning > 5\n\n\
Context-Aware Analysis:\n\
- Constructors: adjusted thresholds for params/variables\n\
- Test functions: longer functions tolerated\n\
- Configuration: more variables expected\n\
- Factories: more parameters expected"
)]
Functions {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, value_name = "FIELD")]
sort_by: Option<String>,
#[arg(long)]
violations_only: bool,
#[arg(long, default_value = "30")]
sloc_warn: u32,
#[arg(long, default_value = "60")]
sloc_critical: u32,
#[arg(long, default_value = "5")]
params_warn: u32,
#[arg(long, default_value = "8")]
params_critical: u32,
#[arg(long, short = 'd')]
details: bool,
},
#[command(
name = "coupling",
about = "Calculate coupling metrics for modules",
long_about = "Calculate afferent/efferent coupling metrics for architectural analysis.\n\n\
Coupling Metrics:\n\
- Ca (Afferent Coupling): Number of modules that depend ON this module\n\
- Ce (Efferent Coupling): Number of modules this module depends ON\n\
- Instability (I): Ce / (Ca + Ce) - 0=stable, 1=unstable\n\
- Abstractness (A): abstract_types / total_types\n\
- Distance (D): |A + I - 1| - distance from main sequence\n\n\
Main Sequence Analysis:\n\
- Ideal modules lie on the line A + I = 1\n\
- Zone of Pain (A=0, I=0): Concrete and stable - hard to change\n\
- Zone of Uselessness (A=1, I=1): Abstract and unstable\n\n\
Dependency Types Tracked:\n\
- Import: Direct import statements\n\
- Call: Function calls across module boundaries\n\
- Type: Using types from another module\n\
- Inheritance: Class inheritance relationships"
)]
Coupling {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, value_enum, default_value = "file")]
level: CouplingLevelArg,
#[arg(long, short = 's')]
sort: bool,
#[arg(long, short = 't', value_name = "N")]
threshold: Option<f64>,
#[arg(long)]
show_cycles: bool,
#[arg(long)]
show_edges: bool,
},
#[command(
name = "cohesion",
about = "Calculate class cohesion metrics (LCOM)",
long_about = "Calculate Lack of Cohesion of Methods (LCOM) variants for class quality analysis.\n\n\
LCOM measures how tightly related the methods of a class are. High LCOM indicates\n\
a class that might be doing too many things and should be split.\n\n\
LCOM Variants:\n\
- LCOM1: max(0, P - Q) where P = non-sharing pairs, Q = sharing pairs\n\
- LCOM2: P - Q (can be negative, indicates good cohesion)\n\
- LCOM3: Connected components in method-attribute graph\n\
- LCOM4: LCOM3 + method-to-method call edges\n\n\
Interpretation:\n\
- LCOM3 = 1: Perfectly cohesive class\n\
- LCOM3 > 1: Consider splitting into LCOM3 classes\n\
- LCOM4 accounts for methods that call each other\n\n\
Language Support:\n\
- Python: self.attr access, self.method() calls\n\
- TypeScript/JS: this.attr, this.method()\n\
- Rust: self.field, self.method()\n\
- Go: receiver.field access patterns\n\n\
Cohesion Levels:\n\
- High (LCOM3 = 1): Well-designed cohesive class\n\
- Medium (LCOM3 = 2): Minor cohesion issue\n\
- Low (LCOM3 = 3-4): Consider splitting\n\
- VeryLow (LCOM3 >= 5): Strongly recommend splitting"
)]
Cohesion {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, short = 't', value_name = "N")]
threshold: Option<u32>,
#[arg(long, short = 's')]
sort: bool,
#[arg(long)]
violations_only: bool,
#[arg(long)]
show_components: bool,
},
#[command(
name = "report",
about = "Generate comprehensive metrics report",
long_about = "Generate a unified metrics report combining all analyzers.\n\n\
This command runs all metric analyzers in parallel and produces\n\
a comprehensive report with:\n\n\
- Project-level summary (averages, totals)\n\
- Per-file metrics breakdown\n\
- Per-function unified metrics (CC, cognitive, MI, etc.)\n\
- Per-class cohesion metrics\n\
- Detected issues with severity levels\n\n\
Quality Gates:\n\
Use --fail-on critical to fail CI if critical issues are found.\n\
Use --fail-on warning to fail on warnings or critical issues.\n\n\
Threshold Presets:\n\
- default: Industry standard thresholds\n\
- strict: Stricter thresholds for high-quality code\n\
- relaxed: More permissive for legacy codebases"
)]
Report {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, default_value = "default")]
thresholds: String,
#[arg(long, value_name = "FIELD")]
sort_by: Option<String>,
#[arg(long)]
issues_only: bool,
#[arg(long)]
skip_coupling: bool,
#[arg(long, value_name = "LEVEL")]
fail_on: Option<String>,
#[arg(long, default_value = "0")]
max_files: usize,
#[arg(long, default_value = "20")]
top: usize,
#[arg(long)]
show_tokens: bool,
},
}
#[derive(Clone, Copy, ValueEnum, Default, Debug)]
enum SecurityOutputFormat {
#[default]
Json,
Sarif,
Text,
}
#[derive(Subcommand)]
enum SecurityCommands {
#[command(
name = "scan",
about = "Run all security analyzers on a path",
long_about = "Run all security analyzers in parallel and generate a unified report.\n\n\
Analyzers included:\n\
- SQL Injection (CWE-89)\n\
- Command Injection (CWE-78)\n\
- Cross-Site Scripting (XSS) (CWE-79)\n\
- Path Traversal (CWE-22)\n\
- Secrets Detection (CWE-798)\n\
- Weak Cryptography (CWE-327)\n\
- Unsafe Deserialization (CWE-502)\n\
- ReDoS (CWE-1333)\n\n\
Output formats:\n\
- json: Standard JSON output\n\
- sarif: SARIF v2.1 format for CI/CD integration (GitHub, GitLab)\n\
- text: Human-readable text output\n\n\
Suppression:\n\
- Use '# brrr-ignore: SQLI-001' or '// brrr-ignore: CMD-002' to suppress findings\n\
- Also supports 'nosec', 'noqa', 'security-ignore' comment formats\n\n\
Exit codes:\n\
- 0: No findings above fail-on threshold\n\
- 1: Findings found above fail-on threshold\n\n\
Examples:\n\
- brrr security scan . # Scan current directory\n\
- brrr security scan src/ --severity high # Only high/critical issues\n\
- brrr security scan . --format sarif # SARIF output for CI\n\
- brrr security scan . --category injection # Only injection issues\n\
- brrr security scan . --fail-on high # Exit 1 if high+ issues found"
)]
Scan {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: SecurityOutputFormat,
#[arg(long, default_value = "low")]
severity: String,
#[arg(long, default_value = "low")]
confidence: String,
#[arg(long)]
category: Option<String>,
#[arg(long, default_value = "high")]
fail_on: String,
#[arg(long)]
include_suppressed: bool,
#[arg(long, default_value = "0")]
max_files: usize,
},
#[command(
name = "sql-injection",
about = "Scan for SQL injection vulnerabilities",
long_about = "Detect SQL injection vulnerabilities in Python and TypeScript/JavaScript code.\n\n\
Detects unsafe patterns:\n\
- String concatenation: \"SELECT * FROM users WHERE id = \" + user_input\n\
- f-strings (Python): f\"SELECT * FROM users WHERE id = {user_input}\"\n\
- Template literals (TS): `SELECT * FROM users WHERE id = ${userId}`\n\
- Format strings: \"SELECT ... %s\" % user_input\n\n\
Safe patterns NOT flagged:\n\
- Parameterized queries: cursor.execute(\"SELECT ... WHERE id = ?\", (id,))\n\
- ORM methods with proper escaping"
)]
SqlInjection {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, default_value = "low")]
min_severity: String,
},
#[command(
name = "command-injection",
about = "Scan for command injection vulnerabilities",
long_about = "Detect command injection vulnerabilities in source code.\n\n\
Detects dangerous patterns:\n\
- os.system(user_input) - Python shell execution\n\
- subprocess.call(cmd, shell=True) - Python subprocess with shell\n\
- eval(user_input) / exec(user_input) - Code injection\n\
- child_process.exec(cmd) - Node.js shell execution\n\
- Runtime.exec(cmd) - Java process execution\n\
- system(cmd) / popen(cmd) - C/C++ shell execution\n\
- exec.Command(cmd) - Go command execution\n\
- Command::new(cmd) - Rust process spawning\n\n\
Supports: Python, TypeScript/JavaScript, Go, Rust, C/C++, Java\n\n\
Severity levels:\n\
- CRITICAL: Shell execution with user input (os.system, shell=True)\n\
- HIGH: Process execution with user-controlled path\n\
- MEDIUM: Argument injection risk\n\
- LOW: Pattern match only, no confirmed taint"
)]
CommandInjection {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, default_value = "low")]
min_severity: String,
#[arg(long, default_value = "low")]
min_confidence: String,
},
#[command(
name = "xss",
about = "Scan for XSS vulnerabilities",
long_about = "Detect Cross-Site Scripting (XSS) vulnerabilities in JavaScript/TypeScript code.\n\n\
Detects dangerous patterns:\n\
- element.innerHTML = user_input (DOM XSS)\n\
- document.write(user_input)\n\
- insertAdjacentHTML(pos, user_input)\n\
- dangerouslySetInnerHTML={{ __html: user_input }} (React)\n\
- $(selector).html(user_input) (jQuery)\n\
- v-html=\"user_input\" (Vue)\n\
- [innerHTML]=\"user_input\" (Angular)\n\
- eval(user_input) / new Function(user_input)\n\
- setTimeout/setInterval with string argument\n\
- Template literals with HTML: `<div>${user_input}</div>`\n\n\
Safe patterns NOT flagged:\n\
- element.textContent = user_input (safe text assignment)\n\
- element.innerText = user_input (safe text assignment)\n\
- DOMPurify.sanitize(user_input) (sanitized)\n\
- encodeURIComponent(user_input) (encoded)\n\n\
Supports: JavaScript, TypeScript, JSX, TSX, Vue"
)]
Xss {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, default_value = "low")]
min_severity: String,
#[arg(long, default_value = "low")]
min_confidence: String,
},
#[command(
name = "path-traversal",
about = "Scan for path traversal vulnerabilities",
long_about = "Detect path traversal (directory traversal) vulnerabilities in source code.\n\n\
Path traversal allows attackers to access files outside the intended directory\n\
by manipulating path inputs with sequences like '../' or absolute paths.\n\n\
Detects dangerous patterns:\n\
- open(user_input) - Direct file open without validation\n\
- os.path.join(base, user_input) - Still vulnerable to absolute paths and ..\n\
- Path(user_input).read_text() - Python pathlib\n\
- fs.readFile(user_input) - Node.js file system\n\
- std::fs::read(user_input) - Rust file system\n\
- os.Open(path) - Go file system\n\
- fopen(path) - C file operations\n\
- shutil.copy/rmtree with user input - Python high-impact operations\n\
- Hardcoded '../' patterns in strings\n\n\
Safe patterns NOT flagged:\n\
- realpath() + startswith() validation\n\
- os.path.basename() / path.basename() to extract filename\n\
- Allowlist validation against known filenames\n\n\
Supports: Python, TypeScript/JavaScript, Go, Rust, C/C++\n\n\
WARNING: Even with path validation, symlink attacks may still be possible!"
)]
PathTraversal {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, default_value = "low")]
min_severity: String,
#[arg(long, default_value = "low")]
min_confidence: String,
},
#[command(
name = "secrets",
about = "Scan for hardcoded secrets and credentials",
long_about = "Detect hardcoded secrets, API keys, and credentials in source code.\n\n\
Detects secrets from major providers:\n\
- AWS Access Keys (AKIA...)\n\
- GitHub Tokens (ghp_, gho_, ghs_, ghr_, github_pat_...)\n\
- GitLab Tokens (glpat-...)\n\
- Slack Tokens (xox[baprs]-...)\n\
- Stripe Keys (sk_live_, sk_test_, rk_live_...)\n\
- Google API Keys (AIza...)\n\
- OpenAI Keys (sk-..., sk-proj-...)\n\
- Anthropic Keys (sk-ant-...)\n\
- SendGrid Keys (SG....)\n\
- npm/PyPI Tokens\n\
- Private Keys (RSA, EC, SSH, PGP)\n\
- JWTs (eyJ...)\n\
- Database connection strings with credentials\n\n\
Also detects:\n\
- Generic password/api_key/secret assignments\n\
- High-entropy strings (potential secrets)\n\n\
NOT flagged (false positive reduction):\n\
- Environment variable reads: os.environ['KEY'], process.env.KEY\n\
- Config file reads: config.get('password')\n\
- Placeholder values: 'YOUR_API_KEY_HERE', 'changeme', 'xxx'\n\
- Test files (reduced severity)\n\n\
Supports: Python, TypeScript/JavaScript, Go, Rust, Java, C/C++, Ruby, PHP, YAML, JSON, env files"
)]
Secrets {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, default_value = "low")]
min_severity: String,
#[arg(long, default_value = "low")]
min_confidence: String,
#[arg(long, default_value = "true")]
include_entropy: bool,
},
#[command(
name = "crypto",
about = "Scan for weak cryptography usage",
long_about = "Detect weak cryptographic algorithms and insecure patterns in source code.\n\n\
Detects weak algorithms:\n\
- Hash: MD5, SHA-1 (for security purposes)\n\
- Cipher: DES, 3DES, RC4, Blowfish\n\
- RSA < 2048 bits\n\
- ECB mode (reveals patterns in encrypted data)\n\n\
Detects insecure usage:\n\
- Hardcoded encryption keys\n\
- Hardcoded IVs (should be random)\n\
- Predictable random for crypto (random.random(), Math.random())\n\n\
Context-aware detection:\n\
- SAFE: MD5 for file checksums/cache keys (non-security)\n\
- SAFE: SHA-1 for git operations (non-security context)\n\
- UNSAFE: MD5/SHA-1 for passwords, signatures, tokens\n\n\
Supports: Python, TypeScript/JavaScript, Go, Rust, Java, C/C++"
)]
Crypto {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, default_value = "low")]
min_severity: String,
#[arg(long, default_value = "low")]
min_confidence: String,
#[arg(long)]
include_safe: bool,
},
#[command(
name = "deserialization",
about = "Scan for unsafe deserialization vulnerabilities",
long_about = "Detect unsafe deserialization vulnerabilities that can lead to RCE.\n\n\
WHY DESERIALIZATION IS DANGEROUS:\n\
Unsafe deserialization can execute arbitrary code WITHOUT obvious sinks.\n\
Python pickle's __reduce__ protocol executes code during unpickling.\n\
Java gadget chains (Commons Collections, etc.) achieve RCE via object graphs.\n\n\
Detects dangerous patterns:\n\
- Python: pickle.load/loads, yaml.load (without SafeLoader), marshal,\n\
shelve.open, dill, cloudpickle, jsonpickle, torch.load, joblib.load\n\
- Java: ObjectInputStream.readObject(), XMLDecoder, XStream, Kryo, Hessian\n\
- JavaScript/TypeScript: eval(), new Function(), node-serialize,\n\
serialize-javascript, js-yaml (unsafe mode)\n\
- Ruby: Marshal.load, YAML.load (unsafe)\n\
- PHP: unserialize()\n\n\
Safe patterns NOT flagged:\n\
- yaml.safe_load() (Python)\n\
- YAML.safe_load() (Ruby)\n\
- JSON.parse() (inherently safe)\n\
- numpy.load(allow_pickle=False)\n\
- PHP unserialize with allowed_classes=false\n\n\
Data flow tracking:\n\
- HTTP request body/parameters\n\
- File uploads\n\
- Network sockets\n\
- Message queues (Kafka, RabbitMQ, Celery)\n\
- Database fields\n\n\
Supports: Python, TypeScript/JavaScript, Java (Ruby, PHP via patterns)"
)]
Deserialization {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, default_value = "low")]
min_severity: String,
#[arg(long, default_value = "low")]
min_confidence: String,
#[arg(long)]
include_safe: bool,
},
#[command(
name = "redos",
about = "Scan for ReDoS vulnerabilities",
long_about = "Detect Regular Expression Denial of Service (ReDoS) vulnerabilities.\n\n\
ReDoS occurs when a regex can be made to take exponential time\n\
on certain malicious inputs, causing denial of service.\n\n\
Detects dangerous patterns:\n\
- Evil regex with nested quantifiers: (a+)+ (a*)*\n\
- Overlapping alternations: (a|a)+\n\
- Polynomial complexity patterns\n\
- Exponential complexity patterns\n\n\
Patterns in user-facing regex:\n\
- re.match(user_input, text) - Python\n\
- new RegExp(user_input) - JavaScript\n\
- regex::Regex::new(&user_input) - Rust\n\
- Pattern.compile(user_input) - Java\n\
- regexp.Compile(user_input) - Go\n\n\
Supports: Python, TypeScript/JavaScript, Go, Rust, Java"
)]
Redos {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, default_value = "low")]
min_severity: String,
#[arg(long, default_value = "low")]
min_confidence: String,
},
}
#[derive(Subcommand)]
enum QualityCommands {
#[command(
name = "clones",
about = "Detect code clones (duplicate code)",
long_about = "Detect code duplicates (clones) using textual analysis.\n\n\
Type-1 Clone Detection:\n\
- Exact copies ignoring whitespace and comments\n\
- Uses rolling hash (Rabin fingerprint) algorithm\n\
- Memory-efficient streaming for large codebases\n\n\
Automatic Exclusions:\n\
- License headers (MIT, Apache, GPL, etc.)\n\
- Test fixtures (intentional duplication)\n\
- Generated code (protobuf, thrift, etc.)\n\n\
Output includes:\n\
- Clone groups with all instances\n\
- Total duplicated lines and percentage\n\
- File locations and preview snippets"
)]
Clones {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, short = 'm', default_value = "6")]
min_lines: usize,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long)]
include_licenses: bool,
#[arg(long)]
include_tests: bool,
#[arg(long)]
include_generated: bool,
#[arg(long, default_value = "1048576")]
max_file_size: u64,
},
#[command(
name = "structural-clones",
about = "Detect structural code clones (Type-2/Type-3)",
long_about = "Detect code duplicates based on AST structure.\n\n\
Clone Types:\n\
- Type-2: Same structure with different identifiers/literals\n\
- Type-3: Similar structure with small modifications\n\n\
Algorithm:\n\
- Normalizes function ASTs to canonical form\n\
- Replaces identifiers with placeholders ($VAR1, $VAR2)\n\
- Replaces literals with type markers ($INT, $STRING)\n\
- Hashes normalized form for fast Type-2 detection\n\
- Computes edit distance for Type-3 similarity\n\n\
Example (original -> normalized):\n\
def calc_area(width, height):\n\
return width * height\n\
\n\
Normalized: def $FUNC($PARAM1, $PARAM2): return $PARAM1 * $PARAM2\n\n\
Filters out likely false positives:\n\
- Simple getters/setters\n\
- Interface/trait implementations\n\
- Test functions (optional)"
)]
StructuralClones {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, short = 's', default_value = "0.8")]
similarity: f64,
#[arg(long, default_value = "10")]
min_nodes: usize,
#[arg(long, short = 'm', default_value = "3")]
min_lines: usize,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long)]
type2_only: bool,
#[arg(long)]
type3_only: bool,
#[arg(long)]
include_tests: bool,
#[arg(long)]
include_accessors: bool,
#[arg(long)]
include_interface_impls: bool,
#[arg(long)]
show_filtered: bool,
#[arg(long, default_value = "1048576")]
max_file_size: u64,
#[arg(long, default_value = "1000")]
max_comparisons: usize,
},
#[command(
name = "god-class",
about = "Detect God classes (classes that violate Single Responsibility Principle)",
long_about = "Detect God classes using weighted scoring based on multiple indicators.\n\n\
Indicators:\n\
- Method count: Classes with > 20 methods (+2 per excess method)\n\
- Attribute count: Classes with > 15 attributes (+1 per excess attribute)\n\
- Line count: Classes with > 500 lines (+1 per 100 excess lines)\n\
- LCOM (cohesion): Classes with LCOM3 > 2 (+5 per excess component)\n\
- Complexity: Sum of all method complexities\n\n\
Severity Levels:\n\
- Low (score 10-20): Minor issues, consider reviewing\n\
- Medium (score 20-35): Notable issues, should refactor\n\
- High (score 35-50): Serious issues, strongly recommend refactoring\n\
- Critical (score 50+): Severe issues, refactor immediately\n\n\
Automatic Exclusions:\n\
- Test classes (TestCase, *Test, *Spec)\n\
- Generated code (detected by markers in path)\n\n\
Output includes:\n\
- Class name, location, and line range\n\
- Indicator values and score breakdown\n\
- Suggested class splits based on responsibility analysis"
)]
GodClass {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, short = 't', default_value = "10.0")]
threshold: f64,
#[arg(long, default_value = "20")]
method_threshold: u32,
#[arg(long, default_value = "15")]
attribute_threshold: u32,
#[arg(long, default_value = "500")]
line_threshold: u32,
#[arg(long, default_value = "2")]
lcom_threshold: u32,
#[arg(long)]
include_tests: bool,
#[arg(long)]
exclude_framework: bool,
#[arg(long)]
include_generated: bool,
#[arg(long, default_value = "1048576")]
max_file_size: u64,
#[arg(long, default_value = "low")]
min_severity: String,
},
#[command(
name = "long-method",
about = "Detect long methods (methods with too much code/complexity)",
long_about = "Detect methods that are too long and complex using multiple metrics.\n\n\
Detection Metrics:\n\
- Lines of code: Methods > 30 lines (configurable)\n\
- Statements: Methods > 25 statements (configurable)\n\
- Local variables: Methods > 10 variables (configurable)\n\
- Cyclomatic complexity: Methods > 10 (configurable)\n\
- Nesting depth: Methods > 4 levels deep (configurable)\n\
- Parameters: Methods > 5 parameters (configurable)\n\n\
Context-Aware Analysis:\n\
- Test methods: Higher thresholds (often have long setup)\n\
- Constructors: Higher thresholds for DI setup\n\
- Factory methods: Adjusted for object construction\n\
- Configuration methods: Adjusted for initialization\n\n\
Severity Levels:\n\
- Minor: 1 threshold exceeded\n\
- Moderate: 2 thresholds exceeded\n\
- Major: 3 thresholds exceeded\n\
- Critical: 4+ thresholds exceeded\n\n\
Output includes:\n\
- Detailed metrics for each flagged method\n\
- Extraction candidates (loop bodies, conditionals, try blocks)\n\
- Refactoring suggestions with line ranges\n\
- Suggested method names for extractions"
)]
LongMethod {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, default_value = "30")]
max_lines: u32,
#[arg(long, default_value = "25")]
max_statements: u32,
#[arg(long, default_value = "10")]
max_variables: u32,
#[arg(long, default_value = "10")]
max_complexity: u32,
#[arg(long, default_value = "4")]
max_nesting: u32,
#[arg(long, default_value = "5")]
max_parameters: u32,
#[arg(long)]
strict: bool,
#[arg(long)]
lenient: bool,
#[arg(long)]
no_context: bool,
#[arg(long)]
show_suggestions: bool,
#[arg(long, default_value = "minor")]
min_severity: String,
#[arg(long, short = 's')]
sort: bool,
},
#[command(
name = "circular",
about = "Detect circular dependencies in imports, classes, or call graph",
long_about = "Detect circular dependencies at multiple granularity levels.\n\n\
Dependency Levels:\n\
- Package: Package-to-package cycles (directory boundaries)\n\
- Module: File-to-file import cycles (A imports B imports A)\n\
- Class: Class usage cycles (A uses B uses A)\n\
- Function: Call graph cycles (A calls B calls A)\n\n\
Algorithm:\n\
- Uses Tarjan's algorithm for Strongly Connected Components (SCCs)\n\
- O(V + E) complexity for efficient analysis\n\
- Detects nested/overlapping cycles and marks them critical\n\n\
Severity Levels:\n\
- Low: Single-package tight coupling (may be intentional)\n\
- Medium: 2-node cycles\n\
- High: 3-5 node cycles\n\
- Critical: 6+ node cycles or nested cycles\n\n\
Output includes:\n\
- All detected cycles with participants and files\n\
- Severity assessment and test involvement flags\n\
- Breaking suggestions with impact scores\n\
- Recommended refactoring techniques"
)]
Circular {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, short = 'l', value_enum, default_value = "module")]
level: CircularLevelArg,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, default_value = "low")]
min_severity: String,
#[arg(long)]
include_tests: bool,
#[arg(long)]
exclude_intentional: bool,
#[arg(long, default_value = "100")]
max_cycles: usize,
#[arg(long, default_value = "20")]
max_suggestions: usize,
},
#[command(
name = "patterns",
about = "Detect design patterns in code",
long_about = "Detect common design patterns using AST analysis and heuristics.\n\n\
Creational Patterns:\n\
- Singleton: Private constructor, static instance, getInstance()\n\
- Factory: Methods returning interface types, *Factory naming\n\
- Builder: Method chaining, build() method, fluent setters\n\n\
Structural Patterns:\n\
- Adapter: Wraps another class, implements different interface\n\
- Decorator: Wraps same interface, delegates to component\n\
- Proxy: Same interface, controls access to subject\n\n\
Behavioral Patterns:\n\
- Observer: Subscribe/notify pattern, listener collections\n\
- Strategy: Interface with multiple implementations\n\
- Command: execute() method, encapsulates action\n\n\
Modern Patterns:\n\
- Dependency Injection: Constructor injection, interface dependencies\n\
- Repository: Data access abstraction with CRUD methods\n\n\
Confidence Scoring:\n\
- 0.9-1.0: Very high confidence (explicit implementation)\n\
- 0.7-0.9: High confidence (strong heuristic match)\n\
- 0.5-0.7: Medium confidence (partial match)\n\
- 0.3-0.5: Low confidence (weak signals)\n\n\
Language-specific idioms:\n\
- Python: __new__ for singleton\n\
- Rust: traits, lazy_static/OnceCell\n\
- Go: implicit interfaces\n\
- Java/TypeScript: standard OOP patterns"
)]
Patterns {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, short = 'p')]
pattern: Option<String>,
#[arg(long, value_enum)]
lang: Option<Language>,
#[arg(long, value_enum, default_value = "json")]
format: OutputFormat,
#[arg(long, short = 'c', default_value = "0.5")]
min_confidence: f64,
#[arg(long)]
include_tests: bool,
#[arg(long)]
include_generated: bool,
#[arg(long)]
no_modern: bool,
#[arg(long, default_value = "1048576")]
max_file_size: u64,
},
}
#[derive(Subcommand)]
enum SemanticCommands {
#[command(
about = "Build semantic index for project",
long_about = "Build unified semantic index for a project (auto-detects all languages).\nFirst run downloads embedding model (1.3GB default, 80MB for MiniLM)."
)]
Index {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, value_enum, default_value = "all")]
lang: WarmLanguage,
#[arg(long)]
model: Option<String>,
#[arg(long, value_enum, default_value = "auto")]
backend: SemanticBackend,
#[arg(long)]
dimension: Option<usize>,
},
#[command(
about = "Search semantically",
long_about = "Search code using natural language queries.\nSearches unified index (all languages). Requires: brrr semantic index ."
)]
Search {
query: String,
#[arg(default_value = ".")]
path: PathBuf,
#[arg(short, long, default_value = "5")]
k: usize,
#[arg(long)]
expand: bool,
#[arg(long)]
model: Option<String>,
#[arg(long, value_enum, default_value = "auto")]
backend: SemanticBackend,
#[arg(long, value_enum, default_value = "code_search")]
task: SearchTask,
#[arg(long)]
force_reload: bool,
},
#[command(
about = "Pre-load and warm up embedding model for faster first query",
long_about = "Pre-load the embedding model into memory (and GPU if available).\nThis speeds up the first semantic search by eliminating model load time."
)]
Warmup {
#[arg(long)]
model: Option<String>,
},
#[command(
about = "Unload model and free GPU memory",
long_about = "Unload the embedding model from memory to free GPU/RAM.\nUseful when you need to free resources for other tasks."
)]
Unload,
#[command(subcommand)]
Cache(CacheCommands),
#[command(
about = "Show compute device and backend info",
long_about = "Show detected compute device (CUDA, MPS, CPU) and available backends."
)]
Device,
#[command(
about = "Show GPU/memory statistics",
long_about = "Show GPU memory usage and model statistics.\nRequires model to be loaded (run semantic search first)."
)]
Memory,
}
#[derive(Subcommand)]
enum CacheCommands {
#[command(about = "Clear all cached indexes")]
Clear,
#[command(about = "Show cache statistics")]
Stats,
#[command(about = "Invalidate index for a specific project")]
Invalidate {
#[arg(default_value = ".")]
path: PathBuf,
},
}
#[derive(Subcommand)]
enum DaemonCommands {
#[command(
about = "Start daemon for project (background)",
long_about = "Start the brrr daemon for faster repeated queries."
)]
Start {
#[arg(short, long, default_value = ".")]
project: PathBuf,
},
#[command(about = "Stop daemon gracefully")]
Stop {
#[arg(short, long, default_value = ".")]
project: PathBuf,
},
#[command(about = "Check if daemon running")]
Status {
#[arg(short, long, default_value = ".")]
project: PathBuf,
},
#[command(about = "Send raw JSON command to daemon")]
Query {
cmd: String,
#[arg(short, long, default_value = ".")]
project: PathBuf,
},
#[command(about = "Notify daemon of file change (triggers reindex at threshold)")]
Notify {
file: PathBuf,
#[arg(short, long, default_value = ".")]
project: PathBuf,
},
#[command(hide = true)]
Serve {
#[arg(short, long)]
project: PathBuf,
},
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let filter = match cli.verbose {
0 => EnvFilter::new("warn"),
1 => EnvFilter::new("info"),
2 => EnvFilter::new("debug"),
_ => EnvFilter::new("trace"),
};
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.init();
match cli.command {
Commands::Tree {
path,
ext,
show_hidden,
max_depth,
} => {
cmd_tree(&path, &ext, show_hidden, cli.no_ignore, max_depth)?;
}
Commands::Structure { path, lang, limit } => {
cmd_structure(&path, lang, limit, cli.no_ignore)?;
}
Commands::Search {
pattern,
path,
ext,
context,
max,
max_files,
} => {
cmd_search(
&pattern,
&path,
&ext,
context,
max,
max_files,
cli.no_ignore,
)?;
}
Commands::Extract {
file,
base_path,
filter_class,
filter_function,
filter_method,
} => {
cmd_extract(&file, base_path.as_ref(), filter_class, filter_function, filter_method)?;
}
Commands::Context {
entry,
project,
depth,
lang,
} => {
cmd_context(&entry, &project, depth, lang)?;
}
Commands::Cfg {
file,
function,
lang,
format,
} => {
cmd_cfg(&file, &function, lang, format)?;
}
Commands::Dfg {
file,
function,
lang,
} => {
cmd_dfg(&file, &function, lang)?;
}
Commands::Slice {
file,
function,
line,
direction,
var,
lang,
extended,
} => {
cmd_slice(&file, &function, line, direction, var, lang, extended)?;
}
Commands::Calls { path, lang, extended } => {
cmd_calls(&path, lang, extended, cli.no_ignore)?;
}
Commands::Impact {
func,
path,
depth,
file,
lang,
} => {
cmd_impact(&func, &path, depth, file, lang, cli.no_ignore)?;
}
Commands::Dead { path, entry, lang } => {
cmd_dead(&path, &entry, lang, cli.no_ignore)?;
}
Commands::Arch { path, lang } => {
cmd_arch(&path, lang, cli.no_ignore)?;
}
Commands::Imports { file, lang } => {
cmd_imports(&file, lang)?;
}
Commands::Importers { module, path, lang } => {
cmd_importers(&module, &path, lang, cli.no_ignore)?;
}
Commands::Warm {
path,
background,
lang,
} => {
cmd_warm(&path, background, lang, cli.no_ignore)?;
}
Commands::ChangeImpact {
files,
session,
git,
git_base,
lang,
depth,
run,
project,
} => {
cmd_change_impact(
&files,
session,
git,
&git_base,
lang,
depth,
run,
&project,
cli.no_ignore,
)?;
}
Commands::Diagnostics {
target,
project,
no_lint,
format,
lang,
} => {
cmd_diagnostics(&target, project, no_lint, format, lang)?;
}
Commands::Semantic(subcmd) => {
cmd_semantic(subcmd, cli.no_ignore).await?;
}
Commands::Daemon(subcmd) => {
cmd_daemon(subcmd).await?;
}
Commands::Doctor { install, json } => {
cmd_doctor(install, json)?;
}
Commands::Security(subcmd) => {
cmd_security(subcmd)?;
}
Commands::Metrics { path, lang, format, fail_on, cmd } => {
match cmd {
Some(subcmd) => {
cmd_metrics(subcmd)?;
}
None => {
let path = path.unwrap_or_else(|| PathBuf::from("."));
let format = format.unwrap_or(OutputFormat::Text);
let exit_code = cmd_metrics_report(
&path,
lang,
format,
"default", None, false, false, fail_on.as_deref(), 0, 50, false, )?;
if exit_code != 0 {
std::process::exit(exit_code);
}
}
}
}
Commands::Quality(subcmd) => {
cmd_quality(subcmd)?;
}
}
Ok(())
}
fn resolve_project_root(path: &Path) -> PathBuf {
if let Some(root) = get_project_root(path) {
return root;
}
path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}
fn cmd_tree(path: &PathBuf, ext: &[String], show_hidden: bool, no_ignore: bool, max_depth: Option<usize>) -> Result<()> {
require_directory(path)?;
let result = ast::file_tree(path.to_str().context("Invalid path")?, ext, show_hidden, no_ignore, max_depth)
.context("Failed to build file tree")?;
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
Ok(())
}
fn cmd_structure(
path: &PathBuf,
lang: Option<Language>,
limit: usize,
no_ignore: bool,
) -> Result<()> {
require_directory(path)?;
let lang_str = lang
.map(|l| l.to_string())
.unwrap_or_else(|| detect_project_language(path));
let result = ast::code_structure(
path.to_str().context("Invalid path")?,
Some(&lang_str),
limit,
no_ignore,
)
.context("Failed to extract code structure")?;
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
Ok(())
}
fn cmd_search(
pattern: &str,
path: &PathBuf,
ext: &[String],
context_lines: usize,
max_results: usize,
max_files: usize,
no_ignore: bool,
) -> Result<()> {
use std::io::{BufRead, BufReader};
let regex = regex::Regex::new(pattern)
.map_err(|e| anyhow::anyhow!("Invalid regex pattern '{}': {}", pattern, e))?;
if !path.exists() {
return Err(anyhow::anyhow!("Path not found: {}", path.display()));
}
let root_path = path.canonicalize().context("Failed to canonicalize path")?;
let mut walker_builder = ignore::WalkBuilder::new(&root_path);
if no_ignore {
walker_builder
.git_ignore(false)
.git_global(false)
.git_exclude(false)
.ignore(false);
} else {
walker_builder.add_custom_ignore_filename(".brrrignore");
}
walker_builder.hidden(true);
let mut results: Vec<serde_json::Value> = Vec::new();
let mut files_scanned: usize = 0;
let mut total_matches: usize = 0;
let effective_max_results = if max_results == 0 { usize::MAX } else { max_results };
for entry in walker_builder.build() {
if files_scanned >= max_files {
break;
}
if results.len() >= effective_max_results {
break;
}
let entry = match entry {
Ok(e) => e,
Err(_) => continue,
};
let file_path = entry.path();
if !file_path.is_file() {
continue;
}
if !ext.is_empty() {
let file_ext = file_path
.extension()
.and_then(|e| e.to_str())
.map(|e| format!(".{}", e));
let matches_ext = file_ext
.as_ref()
.map(|fe| {
ext.iter().any(|filter_ext| {
let normalized = if filter_ext.starts_with('.') {
filter_ext.clone()
} else {
format!(".{}", filter_ext)
};
fe == &normalized
})
})
.unwrap_or(false);
if !matches_ext {
continue;
}
}
files_scanned += 1;
let file = match std::fs::File::open(file_path) {
Ok(f) => f,
Err(_) => continue, };
let reader = BufReader::new(file);
let lines: Vec<String> = reader
.lines()
.map_while(|l| l.ok()) .collect();
for (line_idx, line) in lines.iter().enumerate() {
if results.len() >= effective_max_results {
break;
}
if regex.is_match(line) {
total_matches += 1;
let start = line_idx.saturating_sub(context_lines);
let end = (line_idx + context_lines + 1).min(lines.len());
let context: Vec<serde_json::Value> = (start..end)
.map(|i| {
serde_json::json!({
"line_number": i + 1, "content": &lines[i],
"is_match": i == line_idx,
})
})
.collect();
let rel_path = file_path
.strip_prefix(&root_path)
.unwrap_or(file_path)
.display()
.to_string();
results.push(serde_json::json!({
"file": rel_path,
"line": line_idx + 1, "match": line,
"context": context,
}));
}
}
}
let result = serde_json::json!({
"pattern": pattern,
"total_matches": total_matches,
"files_scanned": files_scanned,
"results": results,
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
Ok(())
}
fn cmd_extract(
file: &PathBuf,
base_path: Option<&PathBuf>,
filter_class: Option<String>,
filter_function: Option<String>,
filter_method: Option<String>,
) -> Result<()> {
require_file(file)?;
let file_path = file.to_str().context("Invalid file path")?;
let base_path_str = base_path.and_then(|p| p.to_str());
let mut module = ast::extract_file(file_path, base_path_str)
.context("Failed to extract file info")?;
if let Some(class_name) = &filter_class {
module.classes.retain(|c| c.name == *class_name);
module.functions.clear();
} else if let Some(method_spec) = &filter_method {
if let Some((class_name, method_name)) = method_spec.split_once('.') {
module.classes.retain(|c| c.name == class_name);
for class in &mut module.classes {
class.methods.retain(|m| m.name == method_name);
}
}
module.functions.clear();
} else if let Some(func_name) = &filter_function {
module.functions.retain(|f| f.name == *func_name);
module.classes.clear();
}
println!(
"{}",
serde_json::to_string_pretty(&module).context("Failed to serialize output")?
);
Ok(())
}
fn cmd_context(
entry: &str,
project: &PathBuf,
depth: usize,
lang: Option<Language>,
) -> Result<()> {
let project_root = resolve_project_root(project);
let lang_str = lang.map(|l| l.to_string().to_lowercase());
let result = callgraph::get_context_with_lang(
project_root.to_str().context("Invalid project path")?,
entry,
depth,
lang_str.as_deref(),
)
.context("Failed to get context")?;
if let Some(text) = result.get("llm_context").and_then(|v| v.as_str()) {
println!("{}", text);
} else {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
Ok(())
}
fn cmd_cfg(
file: &PathBuf,
function: &str,
lang: Option<Language>,
format: OutputFormat,
) -> Result<()> {
require_file(file)?;
let lang_str = lang.map(|l| l.to_string().to_lowercase());
let cfg_result = cfg::extract_with_language(
file.to_str().context("Invalid file path")?,
function,
lang_str.as_deref(),
)
.context("Failed to extract CFG")?;
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&cfg_result).context("Failed to serialize output")?
);
}
OutputFormat::Mermaid => {
let mermaid = cfg::render::to_mermaid(&cfg_result);
println!("{}", mermaid);
}
OutputFormat::Dot => {
let dot = cfg::render::to_dot(&cfg_result);
println!("{}", dot);
}
OutputFormat::Text => {
println!("Control Flow Graph for: {}", function);
println!("Blocks: {}", cfg_result.blocks.len());
println!("Edges: {}", cfg_result.edges.len());
println!("Complexity: {}", cfg_result.cyclomatic_complexity());
let mut block_ids: Vec<_> = cfg_result.blocks.keys().collect();
block_ids.sort_by_key(|id| id.0);
for id in block_ids {
let block = &cfg_result.blocks[id];
println!(
" Block {}: lines {}-{}",
block.id.0, block.start_line, block.end_line
);
}
}
OutputFormat::Csv => {
println!(
"{}",
serde_json::to_string_pretty(&cfg_result).context("Failed to serialize output")?
);
}
}
Ok(())
}
fn cmd_dfg(file: &PathBuf, function: &str, lang: Option<Language>) -> Result<()> {
require_file(file)?;
let lang_str = lang.map(|l| l.to_string().to_lowercase());
let result = dfg::extract_with_language(
file.to_str().context("Invalid file path")?,
function,
lang_str.as_deref(),
)
.context("Failed to extract DFG")?;
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
Ok(())
}
fn cmd_slice(
file: &PathBuf,
function: &str,
line: usize,
direction: SliceDirection,
var: Option<String>,
lang: Option<Language>,
extended: bool,
) -> Result<()> {
if line == 0 {
anyhow::bail!("Line numbers are 1-indexed. Got 0, expected >= 1");
}
require_file(file)?;
let file_path = file.to_str().context("Invalid file path")?;
let lang_str = lang.map(|l| l.to_string().to_lowercase());
let dfg_info = dfg::extract_with_language(file_path, function, lang_str.as_deref())
.context("Failed to extract data flow graph")?;
let criteria = match &var {
Some(v) => dfg::SliceCriteria::at_line_variable(line, v),
None => dfg::SliceCriteria::at_line(line),
};
let slice_result = match direction {
SliceDirection::Backward => dfg::backward_slice(&dfg_info, &criteria),
SliceDirection::Forward => dfg::forward_slice(&dfg_info, &criteria),
};
let mut result = serde_json::json!({
"lines": slice_result.lines,
"count": slice_result.lines.len(),
});
if extended {
let obj = result.as_object_mut().unwrap();
obj.insert("file".to_string(), serde_json::json!(file_path));
obj.insert("function".to_string(), serde_json::json!(function));
obj.insert("target_line".to_string(), serde_json::json!(line));
obj.insert(
"direction".to_string(),
serde_json::json!(match direction {
SliceDirection::Backward => "backward",
SliceDirection::Forward => "forward",
}),
);
if let Some(ref v) = var {
obj.insert("variable".to_string(), serde_json::json!(v));
}
obj.insert(
"variables_in_slice".to_string(),
serde_json::json!(slice_result.variables),
);
obj.insert(
"metrics".to_string(),
serde_json::json!({
"slice_size": slice_result.metrics.slice_size,
"edges_traversed": slice_result.metrics.edges_traversed,
"slice_ratio": slice_result.metrics.slice_ratio,
"variable_count": slice_result.metrics.variable_count,
}),
);
}
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
Ok(())
}
fn cmd_calls(path: &PathBuf, lang: Option<Language>, extended: bool, no_ignore: bool) -> Result<()> {
require_directory(path)?;
let project_root = resolve_project_root(path);
let detected_lang = lang
.map(|l| l.to_string())
.unwrap_or_else(|| detect_project_language(&project_root));
let graph = callgraph::get_or_build_graph_with_config(
&project_root,
Some(&detected_lang),
no_ignore,
)
.context("Failed to build call graph")?;
let edges: Vec<serde_json::Value> = graph.edges.iter().map(|e| {
let mut edge = serde_json::json!({
"from_file": e.caller.file,
"from_func": e.caller.name,
"to_file": e.callee.file,
"to_func": e.callee.name,
});
if extended {
edge["call_line"] = serde_json::json!(e.call_line);
}
edge
}).collect();
let result = serde_json::json!({
"edges": edges,
"count": graph.edges.len(),
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
Ok(())
}
fn cmd_impact(
func: &str,
path: &PathBuf,
depth: usize,
file_filter: Option<String>,
lang: Option<Language>,
no_ignore: bool,
) -> Result<()> {
require_directory(path)?;
let project_root = resolve_project_root(path);
let detected_lang = lang
.map(|l| l.to_string())
.unwrap_or_else(|| detect_project_language(&project_root));
let graph = callgraph::get_or_build_graph_with_config(
&project_root,
Some(&detected_lang),
no_ignore,
)
.context("Failed to build call graph")?;
let config = callgraph::ImpactConfig::new().with_depth(depth);
let result = callgraph::analyze_impact(&graph, func, config);
let callers: Vec<callgraph::FunctionRef> = result
.callers
.into_iter()
.map(|c| callgraph::FunctionRef {
file: c.file,
name: c.name,
qualified_name: c.qualified_name,
})
.collect();
let filtered: Vec<_> = if let Some(filter) = file_filter {
callers
.into_iter()
.filter(|f| f.file.contains(&filter))
.collect()
} else {
callers
};
let result = serde_json::json!({
"function": func,
"callers": filtered.iter().map(|f| {
serde_json::json!({
"file": f.file,
"name": f.name,
"qualified_name": f.qualified_name,
})
}).collect::<Vec<_>>(),
"count": filtered.len(),
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
Ok(())
}
fn cmd_dead(
path: &PathBuf,
entry_points: &[String],
lang: Option<Language>,
no_ignore: bool,
) -> Result<()> {
require_directory(path)?;
let project_root = resolve_project_root(path);
let detected_lang = lang
.map(|l| l.to_string())
.unwrap_or_else(|| detect_project_language(&project_root));
let mut graph = callgraph::get_or_build_graph_with_config(
&project_root,
Some(&detected_lang),
no_ignore,
)
.context("Failed to build call graph")?;
graph.build_indexes();
let mut config = callgraph::DeadCodeConfig::default();
if !entry_points.is_empty() {
config.extra_entry_patterns = entry_points.to_vec();
}
config.language = Some(detected_lang.clone());
let result = callgraph::analyze_dead_code_with_config(&graph, &config);
let output = serde_json::json!({
"dead_functions": result.dead_functions.iter().map(|f| {
serde_json::json!({
"file": f.file,
"name": f.name,
"qualified_name": f.qualified_name,
"line": f.line,
"reason": format!("{:?}", f.reason),
"confidence": f.confidence,
})
}).collect::<Vec<_>>(),
"count": result.total_dead,
"entry_points_used": result.entry_points,
"custom_entry_patterns": entry_points,
"filtered_count": result.filtered_count,
"stats": {
"total_functions": result.stats.total_functions,
"entry_points": result.stats.entry_point_count,
"reachable": result.stats.reachable_count,
"filtered_as_callback": result.stats.filtered_as_callback,
"filtered_as_handler": result.stats.filtered_as_handler,
"filtered_as_decorator": result.stats.filtered_as_decorator,
"filtered_as_dynamic": result.stats.filtered_as_dynamic,
}
});
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
Ok(())
}
fn cmd_arch(path: &PathBuf, lang: Option<Language>, no_ignore: bool) -> Result<()> {
require_directory(path)?;
let project_root = resolve_project_root(path);
let path_str = project_root.to_str().context("Invalid path")?;
let detected_lang = lang
.map(|l| l.to_string())
.unwrap_or_else(|| detect_project_language(&project_root));
let mut graph = callgraph::get_or_build_graph_with_config(
&project_root,
Some(&detected_lang),
no_ignore,
)
.context("Failed to build call graph")?;
graph.build_indexes();
let analysis = callgraph::analyze_architecture(&graph);
let result = serde_json::json!({
"root": path_str,
"layers": {
"entry": {
"count": analysis.entry_functions.len(),
"functions": analysis.entry_functions,
},
"middle": {
"count": analysis.middle_functions.len(),
"functions": analysis.middle_functions,
},
"leaf": {
"count": analysis.leaf_functions.len(),
"functions": analysis.leaf_functions,
},
},
"orphan_functions": analysis.orphan_functions,
"topological_layers": analysis.layers,
"circular_dependencies": analysis.circular_dependencies,
"stats": analysis.stats,
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
Ok(())
}
fn cmd_imports(file: &PathBuf, lang: Option<Language>) -> Result<()> {
require_file(file)?;
let path = std::path::Path::new(file);
let registry = lang::LanguageRegistry::global();
let lang_handler = match lang {
Some(lang_enum) => {
let lang_name = lang_enum.to_string().to_lowercase();
registry.get_by_name(&lang_name).ok_or_else(|| {
anyhow::anyhow!("Unsupported language: {}", lang_name)
})?
}
None => registry.detect_language(path).ok_or_else(|| {
anyhow::anyhow!(
"Could not detect language for: {}",
file.display()
)
})?,
};
let imports = ast::extract_imports(path)
.context("Failed to extract imports")?;
let imports_json: Vec<serde_json::Value> = imports
.iter()
.map(|imp| {
serde_json::json!({
"module": imp.module,
"names": imp.names,
"aliases": imp.aliases,
"is_from": imp.is_from,
"level": imp.level,
"line_number": imp.line_number,
"statement": imp.statement(),
})
})
.collect();
let result = serde_json::json!({
"file": file.display().to_string(),
"language": lang_handler.name(),
"imports": imports_json,
"count": imports_json.len(),
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
Ok(())
}
fn cmd_importers(
module: &str,
path: &PathBuf,
lang: Option<Language>,
no_ignore: bool,
) -> Result<()> {
let project_root = resolve_project_root(path);
let path_str = project_root.to_str().context("Invalid path")?;
let mut config = callgraph::scanner::ScanConfig::default();
let lang_str = lang
.map(|l| l.to_string().to_lowercase())
.unwrap_or_else(|| detect_project_language(&project_root));
config.language = Some(lang_str.clone());
config.no_ignore = no_ignore;
let scanner = callgraph::scanner::ProjectScanner::new(path_str)
.context("Failed to create project scanner")?;
let scan_result = scanner
.scan_with_config(&config)
.context("Failed to scan project")?;
let mut importers = Vec::new();
for file_path in &scan_result.files {
let imports = match ast::extract_imports(file_path) {
Ok(imports) => imports,
Err(_) => continue, };
for imp in &imports {
if import_matches_module(imp, module) {
let rel_path = file_path
.strip_prefix(&project_root)
.unwrap_or(file_path)
.display()
.to_string();
importers.push(serde_json::json!({
"file": rel_path,
"import_statement": imp.statement(),
"module": imp.module,
"names": imp.names,
"line": imp.line_number,
}));
}
}
}
let result = serde_json::json!({
"module": module,
"root": project_root.display().to_string(),
"language": lang_str,
"importers": importers,
"count": importers.len(),
"files_scanned": scan_result.files.len(),
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
Ok(())
}
fn import_matches_module(import: &ast::ImportInfo, module: &str) -> bool {
if import.module == module {
return true;
}
if import.module.ends_with(&format!(".{}", module)) {
return true;
}
if import.module.starts_with(&format!("{}.", module)) {
return true;
}
if import.module.contains(&format!(".{}.", module)) {
return true;
}
if import.names.iter().any(|name| name == module) {
return true;
}
false
}
fn cmd_warm(path: &PathBuf, background: bool, lang: WarmLanguage, no_ignore: bool) -> Result<()> {
let project_root = resolve_project_root(path);
let path_str = project_root.to_str().context("Invalid path")?;
if background {
let exe = std::env::current_exe().context("Failed to get current executable")?;
let mut cmd = Command::new(&exe);
cmd.arg("warm");
cmd.arg(path_str);
cmd.args(["--lang", &lang.to_string()]);
if no_ignore {
cmd.arg("--no-ignore");
}
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
let child = cmd.spawn().context("Failed to spawn background process")?;
println!("Background indexing started (PID: {})", child.id());
println!("Project: {}", path_str);
println!("Language: {}", lang);
return Ok(());
}
let lang_str = lang.to_string();
let lang_filter = if lang_str == "all" {
None
} else {
Some(lang_str.as_str())
};
callgraph::warm_cache_with_config(&project_root, lang_filter, no_ignore)
.context("Failed to warm cache")?;
println!("Cache warmed successfully for {}", project_root.display());
Ok(())
}
fn get_git_changed_files(project: &PathBuf, git_base: &str) -> Result<Vec<PathBuf>> {
let output = std::process::Command::new("git")
.arg("-C")
.arg(project)
.arg("diff")
.arg("--name-only")
.arg(format!("{}...HEAD", git_base))
.output()
.context("Failed to run git diff")?;
if !output.status.success() {
let output = std::process::Command::new("git")
.arg("-C")
.arg(project)
.arg("diff")
.arg("--name-only")
.arg(git_base)
.output()
.context("Failed to run git diff")?;
if !output.status.success() {
anyhow::bail!(
"git diff failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let files: Vec<PathBuf> = String::from_utf8_lossy(&output.stdout)
.lines()
.filter(|l| !l.is_empty())
.map(|l| project.join(l))
.collect();
return Ok(files);
}
let files: Vec<PathBuf> = String::from_utf8_lossy(&output.stdout)
.lines()
.filter(|l| !l.is_empty())
.map(|l| project.join(l))
.collect();
Ok(files)
}
fn get_session_dirty_files(project: &PathBuf) -> Result<Vec<PathBuf>> {
let output = std::process::Command::new("git")
.arg("-C")
.arg(project)
.arg("status")
.arg("--porcelain")
.output()
.context("Failed to run git status")?;
if !output.status.success() {
anyhow::bail!(
"git status failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let files: Vec<PathBuf> = String::from_utf8_lossy(&output.stdout)
.lines()
.filter(|l| !l.is_empty())
.filter_map(|l| {
if l.len() > 3 {
Some(project.join(l[3..].trim()))
} else {
None
}
})
.collect();
Ok(files)
}
fn is_test_file_path(file: &str) -> bool {
let path_lower = file.to_lowercase();
if path_lower.contains("/test/")
|| path_lower.contains("/tests/")
|| path_lower.contains("/__tests__/")
|| path_lower.contains("/spec/")
|| path_lower.contains("/specs/")
|| path_lower.starts_with("test/")
|| path_lower.starts_with("tests/")
|| path_lower.starts_with("__tests__/")
{
return true;
}
let filename = file
.rsplit(|c| c == '/' || c == '\\')
.next()
.unwrap_or(file);
let filename_lower = filename.to_lowercase();
if filename_lower.starts_with("test_") && filename_lower.ends_with(".py") {
return true;
}
if filename_lower.ends_with("_test.py") {
return true;
}
if filename_lower.ends_with(".test.js")
|| filename_lower.ends_with(".test.ts")
|| filename_lower.ends_with(".test.jsx")
|| filename_lower.ends_with(".test.tsx")
|| filename_lower.ends_with(".spec.js")
|| filename_lower.ends_with(".spec.ts")
{
return true;
}
if filename_lower.ends_with("_test.go") {
return true;
}
if filename_lower.ends_with("_test.rs") || filename_lower == "tests.rs" {
return true;
}
if filename.ends_with("Test.java")
|| filename.ends_with("Test.kt")
|| filename.ends_with("Tests.java")
|| filename.ends_with("Tests.kt")
{
return true;
}
false
}
fn is_test_function_name(name: &str) -> bool {
if name.starts_with("test_") || name.ends_with("_test") {
return true;
}
if name.starts_with("test") && name.chars().nth(4).map_or(false, |c| c.is_uppercase()) {
return true;
}
if name.ends_with("Test") || name.ends_with("Tests") {
return true;
}
if name.starts_with("it_") || name.starts_with("should_") || name.starts_with("spec_") {
return true;
}
if name.ends_with("_spec") {
return true;
}
if name == "setUp"
|| name == "tearDown"
|| name == "setUpClass"
|| name == "tearDownClass"
|| name == "beforeEach"
|| name == "afterEach"
|| name == "beforeAll"
|| name == "afterAll"
{
return true;
}
false
}
fn run_affected_tests(
affected_tests: &[serde_json::Value],
project: &PathBuf,
) -> Result<()> {
let mut test_files: std::collections::HashSet<String> = std::collections::HashSet::new();
for test in affected_tests {
if let Some(file) = test.get("test_file").and_then(|f| f.as_str()) {
test_files.insert(file.to_string());
}
}
if test_files.is_empty() {
println!("No tests to run.");
return Ok(());
}
let is_python = test_files.iter().any(|f| f.ends_with(".py"));
let is_js = test_files
.iter()
.any(|f| f.ends_with(".js") || f.ends_with(".ts"));
let is_rust = test_files.iter().any(|f| f.ends_with(".rs"));
let is_go = test_files.iter().any(|f| f.ends_with(".go"));
if is_python {
let files: Vec<&str> = test_files.iter().map(|s| s.as_str()).collect();
println!("Running pytest for {} test files...", files.len());
let status = std::process::Command::new("pytest")
.args(&files)
.current_dir(project)
.status()
.context("Failed to run pytest")?;
if !status.success() {
anyhow::bail!("pytest failed with status: {}", status);
}
} else if is_js {
println!("Running npm test...");
let status = std::process::Command::new("npm")
.args(["test", "--"])
.args(test_files.iter())
.current_dir(project)
.status()
.context("Failed to run npm test")?;
if !status.success() {
anyhow::bail!("npm test failed with status: {}", status);
}
} else if is_rust {
println!("Running cargo test...");
let status = std::process::Command::new("cargo")
.arg("test")
.current_dir(project)
.status()
.context("Failed to run cargo test")?;
if !status.success() {
anyhow::bail!("cargo test failed with status: {}", status);
}
} else if is_go {
println!("Running go test...");
let status = std::process::Command::new("go")
.args(["test", "./..."])
.current_dir(project)
.status()
.context("Failed to run go test")?;
if !status.success() {
anyhow::bail!("go test failed with status: {}", status);
}
} else {
println!("Unknown test framework. Test files: {:?}", test_files);
}
Ok(())
}
fn cmd_change_impact(
files: &[PathBuf],
session: bool,
git: bool,
git_base: &str,
lang: Option<Language>,
depth: usize,
run_tests: bool,
project: &PathBuf,
_no_ignore: bool,
) -> Result<()> {
let project_str = project.to_str().context("Invalid project path")?;
let changed_files: Vec<PathBuf> = if !files.is_empty() {
files.to_vec()
} else if git {
get_git_changed_files(project, git_base)?
} else if session {
get_session_dirty_files(project)?
} else {
get_git_changed_files(project, git_base)
.or_else(|_| get_session_dirty_files(project))
.unwrap_or_default()
};
if changed_files.is_empty() {
let result = serde_json::json!({
"changed_files": [],
"affected_tests": [],
"count": 0,
"message": "No changed files detected"
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
return Ok(());
}
let graph = callgraph::build(project_str).context("Failed to build call graph")?;
let mut affected_tests: Vec<serde_json::Value> = Vec::new();
let mut seen_tests: std::collections::HashSet<(String, String)> =
std::collections::HashSet::new();
let registry = lang::LanguageRegistry::global();
let project_root = resolve_project_root(project);
let project_lang = detect_project_language(&project_root);
for file in &changed_files {
let file_str = file.to_string_lossy().to_string();
if is_test_file_path(&file_str) {
continue;
}
let file_lang = lang
.map(|l| l.to_string())
.or_else(|| registry.detect_language(file).map(|l| l.name().to_string()))
.unwrap_or_else(|| project_lang.clone());
let _ = file_lang;
if !file.exists() {
continue;
}
let module = match ast::extract_file(file.to_str().unwrap_or_default(), None) {
Ok(m) => m,
Err(_) => continue, };
for func in &module.functions {
let config = callgraph::ImpactConfig::new()
.with_depth(depth)
.with_call_sites();
let impact = callgraph::impact::analyze_impact(&graph, &func.name, config);
for caller in &impact.callers {
let is_test = is_test_file_path(&caller.file) || is_test_function_name(&caller.name);
if is_test {
let test_key = (caller.file.clone(), caller.name.clone());
if seen_tests.insert(test_key) {
affected_tests.push(serde_json::json!({
"test_file": caller.file,
"test_function": caller.name,
"changed_file": file_str,
"changed_function": func.name,
"distance": caller.distance,
}));
}
}
}
}
for class in &module.classes {
for method in &class.methods {
let qualified_name = format!("{}.{}", class.name, method.name);
for search_name in [&qualified_name, &method.name] {
let config = callgraph::ImpactConfig::new()
.with_depth(depth)
.with_call_sites();
let impact = callgraph::impact::analyze_impact(&graph, search_name, config);
for caller in &impact.callers {
let is_test =
is_test_file_path(&caller.file) || is_test_function_name(&caller.name);
if is_test {
let test_key = (caller.file.clone(), caller.name.clone());
if seen_tests.insert(test_key) {
affected_tests.push(serde_json::json!({
"test_file": caller.file,
"test_function": caller.name,
"changed_file": file_str,
"changed_function": qualified_name,
"distance": caller.distance,
}));
}
}
}
}
}
}
}
affected_tests.sort_by(|a, b| {
let file_a = a.get("test_file").and_then(|f| f.as_str()).unwrap_or("");
let file_b = b.get("test_file").and_then(|f| f.as_str()).unwrap_or("");
let name_a = a.get("test_function").and_then(|f| f.as_str()).unwrap_or("");
let name_b = b.get("test_function").and_then(|f| f.as_str()).unwrap_or("");
(file_a, name_a).cmp(&(file_b, name_b))
});
let output = serde_json::json!({
"changed_files": changed_files.iter().map(|p| p.display().to_string()).collect::<Vec<_>>(),
"affected_tests": affected_tests,
"count": affected_tests.len(),
});
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
if run_tests && !affected_tests.is_empty() {
println!("\n--- Running affected tests ---\n");
run_affected_tests(&affected_tests, project)?;
}
Ok(())
}
fn cmd_diagnostics(
target: &PathBuf,
project_mode: bool,
no_lint: bool,
format: OutputFormat,
lang: Option<Language>,
) -> Result<()> {
require_exists(target)?;
let lang_str = lang.map(|l| match l {
Language::Python => "python",
Language::Typescript => "typescript",
Language::Javascript => "javascript",
Language::Go => "go",
Language::Rust => "rust",
Language::Java => "java",
Language::C => "c",
Language::Cpp => "cpp",
Language::Ruby => "ruby",
Language::Php => "php",
Language::Kotlin => "kotlin",
Language::Swift => "swift",
Language::Csharp => "csharp",
Language::Scala => "scala",
Language::Lua => "lua",
Language::Elixir => "elixir",
});
let include_lint = !no_lint;
let result = if project_mode || target.is_dir() {
diagnostics::get_project_diagnostics(target, lang_str, include_lint)
.context("Failed to run project diagnostics")?
} else {
diagnostics::get_diagnostics(target, lang_str, include_lint)
.context("Failed to run diagnostics")?
};
match format {
OutputFormat::Json => {
let json_output = serde_json::json!({
"target": result.target,
"language": result.language,
"tools": result.tools,
"diagnostics": result.diagnostics,
"error_count": result.error_count,
"warning_count": result.warning_count,
"file_count": result.file_count,
});
println!(
"{}",
serde_json::to_string_pretty(&json_output).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("{}", diagnostics::format_diagnostics_text(&result));
}
_ => {
let json_output = serde_json::json!({
"target": result.target,
"language": result.language,
"tools": result.tools,
"diagnostics": result.diagnostics,
"error_count": result.error_count,
"warning_count": result.warning_count,
"file_count": result.file_count,
});
println!(
"{}",
serde_json::to_string_pretty(&json_output).context("Failed to serialize output")?
);
}
}
Ok(())
}
async fn cmd_semantic_index(
path: &PathBuf,
lang: WarmLanguage,
model: Option<String>,
backend: SemanticBackend,
dimension: Option<usize>,
) -> Result<()> {
use go_brrr::embedding::{IndexConfig, Metric, TeiClient, TeiClientConfig, VectorIndex};
use go_brrr::semantic::{build_embedding_text, extract_units};
use std::time::Instant;
let start_time = Instant::now();
let root = resolve_project_root(path);
let root_str = root.to_string_lossy();
let model_name = model.unwrap_or_else(|| "bge-large-en-v1.5".to_string());
let lang_str = lang.to_string();
let language_filter = if lang_str == "all" { "python" } else { &lang_str };
eprintln!("Extracting semantic units from: {}", root_str);
let units = if lang_str == "all" {
let languages = ["python", "typescript", "javascript", "go", "rust", "java", "c", "cpp"];
let mut all_units = Vec::new();
for lang in &languages {
match extract_units(&root_str, lang) {
Ok(lang_units) => {
if !lang_units.is_empty() {
eprintln!(" Found {} {} units", lang_units.len(), lang);
all_units.extend(lang_units);
}
}
Err(e) => {
tracing::debug!("Failed to extract {} units: {}", lang, e);
}
}
}
all_units
} else {
extract_units(&root_str, language_filter)
.context("Failed to extract semantic units")?
};
if units.is_empty() {
let result = serde_json::json!({
"status": "complete",
"root": root_str,
"language": lang_str,
"model": model_name,
"units_indexed": 0,
"message": "No code units found to index",
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
return Ok(());
}
eprintln!("Found {} total units to index", units.len());
let texts: Vec<String> = units.iter().map(|u| build_embedding_text(u)).collect();
eprintln!("Built embedding texts for {} units", texts.len());
let embeddings: Vec<Vec<f32>> = match backend {
SemanticBackend::Tei | SemanticBackend::Auto => {
let tei_config = TeiClientConfig::from_env();
eprintln!("Connecting to TEI server at {}", tei_config.endpoint);
let client = match TeiClient::with_config(tei_config).await {
Ok(c) => c,
Err(e) => {
if matches!(backend, SemanticBackend::Auto) {
let result = serde_json::json!({
"status": "error",
"root": root_str,
"language": lang_str,
"model": model_name,
"units_found": units.len(),
"error": format!(
"No embedding backend available. TEI server connection failed: {}. \
Local sentence-transformers backend is not yet implemented in Rust CLI. \
Please start a TEI server with: \
docker run --gpus all -p 18080:80 ghcr.io/huggingface/text-embeddings-inference:1.7 \
--model-id BAAI/bge-large-en-v1.5",
e
),
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
return Ok(());
} else {
anyhow::bail!(
"Failed to connect to TEI server: {}. \
Start a TEI server or use --backend auto.",
e
);
}
}
};
let server_info = client.info().await.context("Failed to get TEI server info")?;
eprintln!(
"Connected to TEI server: model={}, max_input_length={}",
server_info.model_id, server_info.max_input_length
);
let text_refs: Vec<&str> = texts.iter().map(String::as_str).collect();
eprintln!("Generating embeddings for {} texts...", text_refs.len());
let mrl_dimension = dimension.map(|d| d as u32);
client
.embed_with_options(&text_refs, true, true, mrl_dimension)
.await
.context("Failed to generate embeddings")?
}
SemanticBackend::SentenceTransformers => {
let result = serde_json::json!({
"status": "error",
"root": root_str,
"language": lang_str,
"model": model_name,
"units_found": units.len(),
"error": "Local sentence-transformers backend is not yet implemented in Rust CLI. \
Use --backend tei with a running TEI server, or --backend auto to auto-detect.",
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
return Ok(());
}
};
if embeddings.is_empty() {
anyhow::bail!("TEI server returned no embeddings");
}
let embedding_dim = embeddings[0].len();
eprintln!(
"Generated {} embeddings with {} dimensions",
embeddings.len(),
embedding_dim
);
let index_dir = root.join(".brrr_index");
std::fs::create_dir_all(&index_dir).context("Failed to create index directory")?;
let index_path = index_dir.join("vectors.usearch");
let metadata_path = index_dir.join("metadata.json");
let config = IndexConfig::new(embedding_dim).with_metric(Metric::InnerProduct);
let index = VectorIndex::with_config(config).context("Failed to create vector index")?;
index
.reserve(embeddings.len())
.context("Failed to reserve index capacity")?;
for (i, embedding) in embeddings.iter().enumerate() {
index
.add(i as u64, embedding)
.with_context(|| format!("Failed to add embedding {} to index", i))?;
}
eprintln!("Built vector index with {} vectors", index.len());
index
.save(&index_path)
.context("Failed to save vector index")?;
eprintln!("Saved vector index to: {}", index_path.display());
let metadata = serde_json::json!({
"version": "1.0",
"model": model_name,
"language": lang_str,
"dimension": embedding_dim,
"count": units.len(),
"units": units,
});
let metadata_file = std::fs::File::create(&metadata_path)
.context("Failed to create metadata file")?;
serde_json::to_writer_pretty(metadata_file, &metadata)
.context("Failed to write metadata")?;
eprintln!("Saved metadata to: {}", metadata_path.display());
let elapsed = start_time.elapsed();
let result = serde_json::json!({
"status": "complete",
"root": root_str,
"language": lang_str,
"model": model_name,
"units_indexed": units.len(),
"dimension": embedding_dim,
"index_path": index_path.display().to_string(),
"metadata_path": metadata_path.display().to_string(),
"elapsed_secs": elapsed.as_secs_f64(),
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
Ok(())
}
async fn cmd_semantic_search(
query: &str,
path: &PathBuf,
k: usize,
expand: bool,
backend: SemanticBackend,
task: SearchTask,
) -> Result<()> {
use go_brrr::embedding::{
get_cached_query_embedding, query_in_cache, TeiClient, TeiClientConfig, VectorIndex,
};
use go_brrr::semantic::EmbeddingUnit;
use std::time::Instant;
let start_time = Instant::now();
let root = resolve_project_root(path);
let root_str = root.to_string_lossy();
let num_results = if k == 0 { 10 } else { k };
let query_for_embedding = if task != SearchTask::Default {
let formatted = format!("Instruct: {}\nQuery: {}", task.as_task_string(), query);
eprintln!("Using task-aware query with instruction: {}", task.as_task_string());
formatted
} else {
query.to_string()
};
let index_dir = root.join(".brrr_index");
let index_path = index_dir.join("vectors.usearch");
let metadata_path = index_dir.join("metadata.json");
if !index_path.exists() {
let result = serde_json::json!({
"status": "error",
"query": query,
"root": root_str,
"error": format!(
"No semantic index found at {}. Run 'brrr semantic index' first to build the index.",
index_path.display()
),
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
return Ok(());
}
if !metadata_path.exists() {
let result = serde_json::json!({
"status": "error",
"query": query,
"root": root_str,
"error": format!(
"Metadata file not found at {}. The index may be corrupted. Run 'brrr semantic index' to rebuild.",
metadata_path.display()
),
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
return Ok(());
}
eprintln!("Loading index metadata from: {}", metadata_path.display());
let metadata_file =
std::fs::File::open(&metadata_path).context("Failed to open metadata file")?;
let metadata: serde_json::Value =
serde_json::from_reader(metadata_file).context("Failed to parse metadata JSON")?;
let units: Vec<EmbeddingUnit> = serde_json::from_value(
metadata
.get("units")
.cloned()
.unwrap_or(serde_json::json!([])),
)
.context("Failed to parse units from metadata")?;
if units.is_empty() {
let result = serde_json::json!({
"status": "complete",
"query": query,
"root": root_str,
"num_results": 0,
"results": [],
"message": "Index contains no units",
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
return Ok(());
}
let index_model = metadata
.get("model")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let index_dimension = metadata
.get("dimension")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
eprintln!(
"Loaded metadata: {} units, model={}, dimension={}",
units.len(),
index_model,
index_dimension
);
eprintln!("Loading vector index from: {}", index_path.display());
let index = VectorIndex::restore(&index_path).context("Failed to load vector index")?;
eprintln!(
"Loaded index: {} vectors, {} dimensions",
index.len(),
index.dimensions()
);
if index.is_empty() {
let result = serde_json::json!({
"status": "complete",
"query": query,
"root": root_str,
"num_results": 0,
"results": [],
"message": "Index contains no vectors",
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
return Ok(());
}
let query_embedding: Vec<f32> = match backend {
SemanticBackend::Tei | SemanticBackend::Auto => {
if query_in_cache(&query_for_embedding).unwrap_or(false) {
eprintln!("Query embedding found in cache");
get_cached_query_embedding(&query_for_embedding, |_| {
unreachable!("Cache hit should not call compute function")
})
.context("Failed to get cached query embedding")?
} else {
let tei_config = TeiClientConfig::from_env();
eprintln!("Connecting to TEI server at {}", tei_config.endpoint);
let client = match TeiClient::with_config(tei_config).await {
Ok(c) => c,
Err(e) => {
if matches!(backend, SemanticBackend::Auto) {
let result = serde_json::json!({
"status": "error",
"query": query,
"root": root_str,
"error": format!(
"No embedding backend available. TEI server connection failed: {}. \
Local sentence-transformers backend is not yet implemented in Rust CLI. \
Please start a TEI server with: \
docker run --gpus all -p 18080:80 ghcr.io/huggingface/text-embeddings-inference:1.7 \
--model-id BAAI/bge-large-en-v1.5",
e
),
});
println!(
"{}",
serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?
);
return Ok(());
} else {
anyhow::bail!(
"Failed to connect to TEI server: {}. \
Start a TEI server or use --backend auto.",
e
);
}
}
};
eprintln!("Computing query embedding...");
let embeddings = client
.embed(&[&query_for_embedding])
.await
.context("Failed to generate query embedding")?;
let embedding = embeddings
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("No embedding returned for query"))?;
get_cached_query_embedding(&query_for_embedding, |_| Ok(embedding.clone()))
.context("Failed to cache query embedding")?
}
}
SemanticBackend::SentenceTransformers => {
let result = serde_json::json!({
"status": "error",
"query": query,
"root": root_str,
"error": "Local sentence-transformers backend is not yet implemented in Rust CLI. \
Use --backend tei with a running TEI server, or --backend auto to auto-detect.",
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
return Ok(());
}
};
if query_embedding.len() != index.dimensions() {
let result = serde_json::json!({
"status": "error",
"query": query,
"root": root_str,
"error": format!(
"Dimension mismatch: query embedding has {} dimensions but index has {}. \
The TEI server may be using a different model than what was used to build the index.",
query_embedding.len(),
index.dimensions()
),
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
return Ok(());
}
eprintln!("Searching for {} nearest neighbors...", num_results);
let search_results = index
.search(&query_embedding, num_results)
.context("Failed to search index")?;
let distances: Vec<f32> = search_results.iter().map(|(_, d)| *d).collect();
let scores = index.to_similarity_scores(&distances);
let mut formatted_results: Vec<serde_json::Value> = Vec::new();
for ((key, _distance), score) in search_results.iter().zip(scores.iter()) {
let unit_index = *key as usize;
if unit_index >= units.len() {
tracing::warn!(
"Search returned key {} but only {} units in metadata",
unit_index,
units.len()
);
continue;
}
let unit = &units[unit_index];
let mut result_json = serde_json::json!({
"score": score,
"file": unit.file,
"name": unit.name,
"kind": unit.kind.as_str(),
"line": unit.start_line,
"preview": truncate_code_preview(&unit.code, 200),
});
if !unit.signature.is_empty() {
result_json["signature"] = serde_json::json!(unit.signature);
}
if let Some(ref docstring) = unit.docstring {
result_json["docstring"] = serde_json::json!(truncate_code_preview(docstring, 150));
}
if !unit.semantic_tags.is_empty() {
result_json["semantic_tags"] = serde_json::json!(unit.semantic_tags);
}
if expand {
if !unit.calls.is_empty() {
result_json["calls"] = serde_json::json!(unit.calls);
}
if !unit.called_by.is_empty() {
result_json["called_by"] = serde_json::json!(unit.called_by);
}
}
formatted_results.push(result_json);
}
let elapsed = start_time.elapsed();
let result = serde_json::json!({
"status": "complete",
"query": query,
"root": root_str,
"num_results": formatted_results.len(),
"results": formatted_results,
"elapsed_secs": elapsed.as_secs_f64(),
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
Ok(())
}
fn truncate_code_preview(code: &str, max_chars: usize) -> String {
if code.len() <= max_chars {
return code.to_string();
}
let truncated = &code[..max_chars];
if let Some(pos) = truncated.rfind('\n') {
if pos > max_chars / 2 {
return format!("{}...", &code[..pos]);
}
}
if let Some(pos) = truncated.rfind(char::is_whitespace) {
if pos > max_chars / 2 {
return format!("{}...", &code[..pos]);
}
}
format!("{}...", truncated)
}
async fn cmd_semantic(subcmd: SemanticCommands, _no_ignore: bool) -> Result<()> {
match subcmd {
SemanticCommands::Index {
path,
lang,
model,
backend,
dimension,
} => {
cmd_semantic_index(&path, lang, model, backend, dimension).await?;
}
SemanticCommands::Search {
query,
path,
k,
expand,
model: _,
backend,
task,
force_reload: _,
} => {
cmd_semantic_search(&query, &path, k, expand, backend, task).await?;
}
SemanticCommands::Warmup { model } => {
let result = serde_json::json!({
"model": model.unwrap_or_else(|| "Qwen3-Embedding-0.6B".to_string()),
"status": "not_implemented",
"error": "Model warmup not yet implemented in Rust CLI"
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
SemanticCommands::Unload => {
println!("Model unloading not yet implemented in Rust CLI");
}
SemanticCommands::Cache(cache_cmd) => match cache_cmd {
CacheCommands::Clear => {
println!("Cache cleared (not yet implemented)");
}
CacheCommands::Stats => {
let result = serde_json::json!({
"cached_projects": 0,
"memory_usage_mb": 0,
"error": "Cache stats not yet implemented in Rust CLI"
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
CacheCommands::Invalidate { path } => {
println!("Cache invalidated for: {}", path.display());
}
},
SemanticCommands::Device => {
let result = serde_json::json!({
"device": "cpu",
"device_count": 1,
"total_memory_gb": 0,
"free_memory_gb": 0,
"supports_bf16": false,
"tei_available": false,
"error": "Device detection not yet implemented in Rust CLI"
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
SemanticCommands::Memory => {
let result = serde_json::json!({
"model_loaded": false,
"gpu_memory_used_mb": 0,
"error": "Memory stats not yet implemented in Rust CLI"
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
}
Ok(())
}
fn get_daemon_socket_path(project: &Path) -> PathBuf {
let canonical = project.canonicalize().unwrap_or_else(|_| project.to_path_buf());
let path_str = canonical.to_string_lossy();
let mut hasher = Sha256::new();
hasher.update(path_str.as_bytes());
let result = hasher.finalize();
let hash_hex: String = result.iter()
.take(8)
.map(|b| format!("{b:02x}"))
.collect();
PathBuf::from(format!("/tmp/brrr-{hash_hex}.sock"))
}
#[cfg(unix)]
fn send_daemon_command(socket_path: &Path, command: &serde_json::Value) -> Result<serde_json::Value> {
let mut stream = UnixStream::connect(socket_path)
.context("Failed to connect to daemon socket")?;
stream.set_read_timeout(Some(Duration::from_secs(5)))
.context("Failed to set read timeout")?;
stream.set_write_timeout(Some(Duration::from_secs(5)))
.context("Failed to set write timeout")?;
let cmd_str = serde_json::to_string(command)
.context("Failed to serialize command")?;
stream.write_all(cmd_str.as_bytes())
.context("Failed to write command to socket")?;
stream.write_all(b"\n")
.context("Failed to write newline to socket")?;
stream.flush()
.context("Failed to flush socket")?;
let mut reader = BufReader::new(&stream);
let mut response = String::new();
reader.read_line(&mut response)
.context("Failed to read response from daemon")?;
serde_json::from_str(&response)
.context("Failed to parse daemon response as JSON")
}
#[cfg(not(unix))]
fn send_daemon_command(_socket_path: &Path, _command: &serde_json::Value) -> Result<serde_json::Value> {
anyhow::bail!("Daemon communication is only supported on Unix systems")
}
async fn cmd_daemon(subcmd: DaemonCommands) -> Result<()> {
match subcmd {
DaemonCommands::Start { project } => {
#[cfg(not(unix))]
{
let result = serde_json::json!({
"status": "error",
"project": project.display().to_string(),
"error": "Daemon is only supported on Unix systems"
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
return Ok(());
}
#[cfg(unix)]
{
let canonical = project.canonicalize().unwrap_or_else(|_| project.clone());
let socket_path = get_daemon_socket_path(&canonical);
let project_str = canonical.to_string_lossy().to_string();
if socket_path.exists() {
match send_daemon_command(&socket_path, &serde_json::json!({"cmd": "ping"})) {
Ok(_) => {
let result = serde_json::json!({
"status": "already_running",
"project": project_str,
"socket": socket_path.display().to_string(),
"message": "Daemon is already running for this project"
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
return Ok(());
}
Err(_) => {
std::fs::remove_file(&socket_path).ok();
}
}
}
let exe = std::env::current_exe()
.context("Failed to get current executable path")?;
let child = Command::new(&exe)
.args(["daemon", "serve", "--project", &project_str])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.context("Failed to spawn daemon process")?;
let pid = child.id();
std::thread::sleep(Duration::from_millis(200));
let started = if socket_path.exists() {
send_daemon_command(&socket_path, &serde_json::json!({"cmd": "ping"})).is_ok()
} else {
false
};
let result = serde_json::json!({
"status": if started { "started" } else { "starting" },
"project": project_str,
"pid": pid,
"socket": socket_path.display().to_string(),
"message": if started {
"Daemon started successfully"
} else {
"Daemon is starting (may take a moment to build call graph)"
}
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
}
}
DaemonCommands::Stop { project } => {
let canonical = project.canonicalize().unwrap_or_else(|_| project.clone());
let socket_path = get_daemon_socket_path(&canonical);
if !socket_path.exists() {
let result = serde_json::json!({
"status": "not_running",
"project": canonical.display().to_string(),
"socket": socket_path.display().to_string(),
"message": "No daemon running for this project"
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
return Ok(());
}
match send_daemon_command(&socket_path, &serde_json::json!({"command": "shutdown"})) {
Ok(response) => {
let result = serde_json::json!({
"status": "stopped",
"project": canonical.display().to_string(),
"socket": socket_path.display().to_string(),
"daemon_response": response,
"message": "Daemon stopped successfully"
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
}
Err(_) => {
if let Err(e) = std::fs::remove_file(&socket_path) {
let result = serde_json::json!({
"status": "error",
"project": canonical.display().to_string(),
"socket": socket_path.display().to_string(),
"error": format!("Failed to clean up stale socket: {}", e)
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
} else {
let result = serde_json::json!({
"status": "cleaned",
"project": canonical.display().to_string(),
"socket": socket_path.display().to_string(),
"message": "Cleaned up stale socket (daemon was not responding)"
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
}
}
}
}
DaemonCommands::Status { project } => {
let canonical = project.canonicalize().unwrap_or_else(|_| project.clone());
let socket_path = get_daemon_socket_path(&canonical);
if !socket_path.exists() {
let result = serde_json::json!({
"running": false,
"project": canonical.display().to_string(),
"socket": socket_path.display().to_string()
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
return Ok(());
}
match send_daemon_command(&socket_path, &serde_json::json!({"cmd": "status"})) {
Ok(status) => {
let result = serde_json::json!({
"running": true,
"project": canonical.display().to_string(),
"socket": socket_path.display().to_string(),
"uptime_seconds": status.get("uptime"),
"requests_handled": status.get("requests"),
"cache_stats": status.get("cache"),
"daemon_status": status
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
}
Err(_) => {
let result = serde_json::json!({
"running": false,
"project": canonical.display().to_string(),
"socket": socket_path.display().to_string(),
"stale_socket": true,
"message": "Socket exists but daemon is not responding"
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
}
}
}
DaemonCommands::Query { cmd, project } => {
let canonical = project.canonicalize().unwrap_or_else(|_| project.clone());
let socket_path = get_daemon_socket_path(&canonical);
if !socket_path.exists() {
let result = serde_json::json!({
"status": "error",
"command": cmd,
"project": canonical.display().to_string(),
"error": "No daemon running for this project"
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
return Ok(());
}
match send_daemon_command(&socket_path, &serde_json::json!({"cmd": cmd})) {
Ok(response) => {
let result = serde_json::json!({
"status": "ok",
"command": cmd,
"project": canonical.display().to_string(),
"result": response
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
}
Err(e) => {
let result = serde_json::json!({
"status": "error",
"command": cmd,
"project": canonical.display().to_string(),
"error": format!("Failed to query daemon: {}", e)
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
}
}
}
DaemonCommands::Notify { file, project } => {
let canonical = project.canonicalize().unwrap_or_else(|_| project.clone());
let socket_path = get_daemon_socket_path(&canonical);
if !socket_path.exists() {
let result = serde_json::json!({
"status": "error",
"file": file.display().to_string(),
"project": canonical.display().to_string(),
"error": "No daemon running for this project"
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
return Ok(());
}
let notify_cmd = serde_json::json!({
"cmd": "notify",
"file": file.display().to_string()
});
match send_daemon_command(&socket_path, ¬ify_cmd) {
Ok(response) => {
let result = serde_json::json!({
"status": "ok",
"file": file.display().to_string(),
"project": canonical.display().to_string(),
"result": response
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
}
Err(e) => {
let result = serde_json::json!({
"status": "error",
"file": file.display().to_string(),
"project": canonical.display().to_string(),
"error": format!("Failed to notify daemon: {}", e)
});
println!("{}", serde_json::to_string_pretty(&result)
.context("Failed to serialize output")?);
}
}
}
DaemonCommands::Serve { project } => {
#[cfg(not(unix))]
{
anyhow::bail!("Daemon server is only supported on Unix systems");
}
#[cfg(unix)]
{
cmd_daemon_serve(project).await?;
}
}
}
Ok(())
}
#[cfg(unix)]
async fn cmd_daemon_serve(project: PathBuf) -> Result<()> {
let canonical = project
.canonicalize()
.context("Failed to canonicalize project path")?;
let project_str = canonical.to_string_lossy().to_string();
let socket_path = get_daemon_socket_path(&canonical);
if socket_path.exists() {
std::fs::remove_file(&socket_path)
.context("Failed to remove stale socket")?;
}
let listener = UnixListener::bind(&socket_path)
.context("Failed to bind Unix socket for daemon")?;
listener
.set_nonblocking(true)
.context("Failed to set non-blocking mode on listener")?;
eprintln!("Daemon: Starting for project {}", project_str);
eprintln!("Daemon: Socket at {}", socket_path.display());
eprintln!("Daemon: Building call graph cache...");
let start_time = Instant::now();
let graph = match callgraph::build(&project_str) {
Ok(mut g) => {
g.build_indexes();
let func_count = g.callers.len() + g.callees.len();
eprintln!(
"Daemon: Call graph built in {:?} ({} functions, {} edges)",
start_time.elapsed(),
func_count / 2, g.edges.len()
);
Some(g)
}
Err(e) => {
eprintln!("Daemon: Warning - Failed to build call graph: {}", e);
None
}
};
let last_activity = Arc::new(parking_lot::Mutex::new(Instant::now()));
let shutdown_flag = Arc::new(AtomicBool::new(false));
let request_count = Arc::new(std::sync::atomic::AtomicU64::new(0));
let idle_timeout = Duration::from_secs(30 * 60); let daemon_start = Instant::now();
eprintln!(
"Daemon: Listening (idle timeout: {} min)",
idle_timeout.as_secs() / 60
);
while !shutdown_flag.load(Ordering::Relaxed) {
{
let last = last_activity.lock();
if last.elapsed() > idle_timeout {
eprintln!("Daemon: Idle timeout reached, shutting down");
break;
}
}
match listener.accept() {
Ok((mut stream, _)) => {
*last_activity.lock() = Instant::now();
request_count.fetch_add(1, Ordering::Relaxed);
let mut reader = BufReader::new(&stream);
let mut request_line = String::new();
if reader.read_line(&mut request_line).is_ok() {
let request_line = request_line.trim();
let request: serde_json::Value = match serde_json::from_str(request_line) {
Ok(v) => v,
Err(_) => {
let error_response = serde_json::json!({
"status": "error",
"error": "Invalid JSON request"
});
let _ = writeln!(stream, "{}", error_response);
continue;
}
};
let response = handle_daemon_request(
&request,
&graph,
&shutdown_flag,
daemon_start,
request_count.load(Ordering::Relaxed),
);
if let Err(e) = writeln!(stream, "{}", response) {
eprintln!("Daemon: Failed to send response: {}", e);
}
}
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::sleep(Duration::from_millis(50));
}
Err(e) => {
eprintln!("Daemon: Accept error: {}", e);
}
}
}
eprintln!(
"Daemon: Shutting down (handled {} requests in {:?})",
request_count.load(Ordering::Relaxed),
daemon_start.elapsed()
);
std::fs::remove_file(&socket_path).ok();
Ok(())
}
#[cfg(unix)]
fn handle_daemon_request(
request: &serde_json::Value,
graph: &Option<callgraph::CallGraph>,
shutdown_flag: &AtomicBool,
daemon_start: Instant,
request_count: u64,
) -> String {
let cmd = request
.get("cmd")
.or_else(|| request.get("command"))
.and_then(|v| v.as_str())
.unwrap_or("");
match cmd {
"ping" => {
serde_json::json!({
"status": "ok",
"pong": true
})
.to_string()
}
"status" => {
let has_graph = graph.is_some();
let func_count = graph.as_ref().map(|g| (g.callers.len() + g.callees.len()) / 2).unwrap_or(0);
let edge_count = graph.as_ref().map(|g| g.edges.len()).unwrap_or(0);
serde_json::json!({
"status": "running",
"uptime": daemon_start.elapsed().as_secs(),
"requests": request_count,
"cache": {
"call_graph_loaded": has_graph,
"function_count": func_count,
"edge_count": edge_count
}
})
.to_string()
}
"shutdown" => {
shutdown_flag.store(true, Ordering::Relaxed);
serde_json::json!({
"status": "ok",
"message": "Daemon shutting down"
})
.to_string()
}
"notify" => {
let file = request
.get("file")
.and_then(|v| v.as_str())
.unwrap_or("<unknown>");
serde_json::json!({
"status": "ok",
"acknowledged": true,
"file": file,
"note": "File change tracked (re-indexing not yet implemented)"
})
.to_string()
}
"impact" => {
let func_name = request
.get("function")
.and_then(|v| v.as_str())
.unwrap_or("");
if func_name.is_empty() {
return serde_json::json!({
"status": "error",
"error": "Missing 'function' parameter"
})
.to_string();
}
if let Some(ref g) = graph {
let depth = request
.get("depth")
.and_then(|v| v.as_u64())
.unwrap_or(3) as usize;
let config = callgraph::ImpactConfig::new().with_depth(depth);
let result = callgraph::analyze_impact(g, func_name, config);
let direct_count = result.by_distance.get(&1).copied().unwrap_or(0);
serde_json::json!({
"status": "ok",
"function": func_name,
"callers_count": result.callers.len(),
"direct_callers": direct_count,
"callers": result.callers.iter().take(50).map(|c| {
serde_json::json!({
"name": c.name,
"file": c.file,
"distance": c.distance
})
}).collect::<Vec<_>>()
})
.to_string()
} else {
serde_json::json!({
"status": "error",
"error": "Call graph not loaded"
})
.to_string()
}
}
"dead" => {
if let Some(ref g) = graph {
serde_json::json!({
"status": "ok",
"note": "Dead code analysis via daemon not fully implemented",
"edge_count": g.edges.len()
})
.to_string()
} else {
serde_json::json!({
"status": "error",
"error": "Call graph not loaded"
})
.to_string()
}
}
_ => {
serde_json::json!({
"status": "error",
"error": format!("Unknown command: {}", cmd),
"available_commands": ["ping", "status", "shutdown", "notify", "impact", "dead"]
})
.to_string()
}
}
}
struct ToolInfo {
name: &'static str,
lang: &'static str,
category: &'static str,
install_cmd: &'static str,
version_args: Option<&'static [&'static str]>,
}
impl ToolInfo {
const fn new(
name: &'static str,
lang: &'static str,
category: &'static str,
install_cmd: &'static str,
) -> Self {
Self {
name,
lang,
category,
install_cmd,
version_args: None,
}
}
const fn with_version_args(mut self, args: &'static [&'static str]) -> Self {
self.version_args = Some(args);
self
}
}
fn get_tool_version(tool: &ToolInfo) -> Option<String> {
let args = tool.version_args.unwrap_or(&["--version"]);
let output = Command::new(tool.name).args(args).output().ok()?;
let version_str = if output.stdout.is_empty() {
String::from_utf8_lossy(&output.stderr).to_string()
} else {
String::from_utf8_lossy(&output.stdout).to_string()
};
version_str
.lines()
.find(|line| !line.trim().is_empty())
.map(|line| line.trim().to_string())
}
fn run_install_command(cmd: &str) -> Result<()> {
if cmd.starts_with("http://") || cmd.starts_with("https://") {
println!(" Manual installation required. Visit: {}", cmd);
return Ok(());
}
let parts: Vec<&str> = cmd.split_whitespace().collect();
if parts.is_empty() {
return Ok(());
}
let status = Command::new(parts[0])
.args(&parts[1..])
.status()
.with_context(|| format!("Failed to execute: {}", cmd))?;
if !status.success() {
anyhow::bail!("Install command failed with exit code: {:?}", status.code());
}
Ok(())
}
fn cmd_doctor(install: Option<String>, json_output: bool) -> Result<()> {
let tools: &[ToolInfo] = &[
ToolInfo::new("pyright", "python", "type_checker", "pip install pyright"),
ToolInfo::new("ruff", "python", "linter", "pip install ruff"),
ToolInfo::new("mypy", "python", "type_checker", "pip install mypy"),
ToolInfo::new("black", "python", "formatter", "pip install black"),
ToolInfo::new("tsc", "typescript", "type_checker", "npm install -g typescript"),
ToolInfo::new("eslint", "typescript", "linter", "npm install -g eslint"),
ToolInfo::new("prettier", "typescript", "formatter", "npm install -g prettier"),
ToolInfo::new("go", "go", "type_checker", "https://go.dev/dl/")
.with_version_args(&["version"]),
ToolInfo::new(
"golangci-lint",
"go",
"linter",
"go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest",
),
ToolInfo::new("rustc", "rust", "type_checker", "https://rustup.rs/"),
ToolInfo::new("cargo", "rust", "build", "https://rustup.rs/"),
ToolInfo::new("clippy-driver", "rust", "linter", "rustup component add clippy")
.with_version_args(&["--version"]),
ToolInfo::new("rustfmt", "rust", "formatter", "rustup component add rustfmt"),
ToolInfo::new("javac", "java", "type_checker", "https://adoptium.net/"),
ToolInfo::new("clang", "c", "type_checker", "https://releases.llvm.org/"),
ToolInfo::new("clang-tidy", "c", "linter", "https://releases.llvm.org/"),
ToolInfo::new("clang-format", "c", "formatter", "https://releases.llvm.org/"),
];
if let Some(ref lang) = install {
let lang_tools: Vec<_> = tools.iter().filter(|t| t.lang == lang).collect();
if lang_tools.is_empty() {
let available_langs: std::collections::HashSet<_> =
tools.iter().map(|t| t.lang).collect();
let langs_list: Vec<_> = available_langs.into_iter().collect();
anyhow::bail!(
"Unknown language: '{}'. Available: {}",
lang,
langs_list.join(", ")
);
}
println!("Installing tools for {}...", lang);
println!();
for tool in lang_tools {
let is_installed = which(tool.name).is_ok();
if is_installed {
let version = get_tool_version(tool).unwrap_or_else(|| "unknown".to_string());
println!(" {} already installed ({})", tool.name, version);
} else {
println!(" Installing {}...", tool.name);
match run_install_command(tool.install_cmd) {
Ok(()) => {
if which(tool.name).is_ok() {
let version =
get_tool_version(tool).unwrap_or_else(|| "unknown".to_string());
println!(" Successfully installed {}", version);
} else {
println!(" Installation may have succeeded but tool not found in PATH");
}
}
Err(e) => {
eprintln!(" Failed to install {}: {}", tool.name, e);
}
}
}
}
return Ok(());
}
let mut status_list = Vec::new();
for tool in tools {
let installed = which(tool.name).is_ok();
let version = if installed {
get_tool_version(tool)
} else {
None
};
let path = which(tool.name).ok().map(|p| p.display().to_string());
status_list.push(serde_json::json!({
"tool": tool.name,
"language": tool.lang,
"category": tool.category,
"installed": installed,
"version": version,
"path": path,
"install_command": tool.install_cmd,
}));
}
if json_output {
let mut by_language: std::collections::HashMap<&str, Vec<&serde_json::Value>> =
std::collections::HashMap::new();
for status in &status_list {
let lang = status["language"].as_str().unwrap_or("unknown");
by_language.entry(lang).or_default().push(status);
}
let result = serde_json::json!({
"tools": status_list,
"by_language": by_language,
"summary": {
"total": tools.len(),
"installed": status_list.iter().filter(|s| s["installed"].as_bool().unwrap_or(false)).count(),
"missing": status_list.iter().filter(|s| !s["installed"].as_bool().unwrap_or(false)).count(),
}
});
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
} else {
println!("brrr Diagnostics Check");
println!("{}", "=".repeat(60));
println!();
let mut current_lang = "";
for (i, tool) in tools.iter().enumerate() {
if tool.lang != current_lang {
if !current_lang.is_empty() {
println!();
}
let lang_display = tool
.lang
.chars()
.next()
.map(|c| c.to_uppercase().to_string() + &tool.lang[1..])
.unwrap_or_else(|| tool.lang.to_string());
println!("{}:", lang_display);
current_lang = tool.lang;
}
let status = &status_list[i];
let installed = status["installed"].as_bool().unwrap_or(false);
let icon = if installed { "[OK]" } else { "[ ]" };
if installed {
let version = status["version"]
.as_str()
.unwrap_or("version unknown")
.trim();
let version_display = if version.len() > 40 {
format!("{}...", &version[..37])
} else {
version.to_string()
};
println!(" {} {} - {}", icon, tool.name, version_display);
} else {
println!(
" {} {} - NOT INSTALLED ({})",
icon, tool.name, tool.install_cmd
);
}
}
let installed_count = status_list
.iter()
.filter(|s| s["installed"].as_bool().unwrap_or(false))
.count();
let total = tools.len();
println!();
println!("{}", "-".repeat(60));
println!(
"Summary: {}/{} tools installed",
installed_count, total
);
if installed_count < total {
println!();
println!("To install missing tools for a language:");
println!(" brrr doctor --install <language>");
println!();
println!("Available languages: python, typescript, go, rust, java, c");
}
}
Ok(())
}
fn cmd_security(subcmd: SecurityCommands) -> Result<()> {
match subcmd {
SecurityCommands::Scan {
path,
lang,
format,
severity,
confidence,
category,
fail_on,
include_suppressed,
max_files,
} => {
let exit_code = cmd_security_scan(
&path,
lang,
format,
&severity,
&confidence,
category.as_deref(),
&fail_on,
include_suppressed,
max_files,
)?;
if exit_code != 0 {
std::process::exit(exit_code);
}
}
SecurityCommands::SqlInjection {
path,
lang,
format,
min_severity,
} => {
cmd_sql_injection(&path, lang, format, &min_severity)?;
}
SecurityCommands::CommandInjection {
path,
lang,
format,
min_severity,
min_confidence,
} => {
cmd_command_injection(&path, lang, format, &min_severity, &min_confidence)?;
}
SecurityCommands::Xss {
path,
lang,
format,
min_severity,
min_confidence,
} => {
cmd_xss(&path, lang, format, &min_severity, &min_confidence)?;
}
SecurityCommands::PathTraversal {
path,
lang,
format,
min_severity,
min_confidence,
} => {
cmd_path_traversal(&path, lang, format, &min_severity, &min_confidence)?;
}
SecurityCommands::Secrets {
path,
lang,
format,
min_severity,
min_confidence,
include_entropy,
} => {
cmd_secrets(&path, lang, format, &min_severity, &min_confidence, include_entropy)?;
}
SecurityCommands::Crypto {
path,
lang,
format,
min_severity,
min_confidence,
include_safe,
} => {
cmd_crypto(&path, lang, format, &min_severity, &min_confidence, include_safe)?;
}
SecurityCommands::Deserialization {
path,
lang,
format,
min_severity,
min_confidence,
include_safe,
} => {
cmd_deserialization(&path, lang, format, &min_severity, &min_confidence, include_safe)?;
}
SecurityCommands::Redos {
path,
lang,
format,
min_severity,
min_confidence,
} => {
cmd_redos(&path, lang, format, &min_severity, &min_confidence)?;
}
}
Ok(())
}
fn cmd_sql_injection(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
min_severity: &str,
) -> Result<()> {
use security::injection::sql::{SqlInjectionDetector, Severity};
require_exists(path)?;
let detector = SqlInjectionDetector::new();
let lang_str = lang.map(|l| l.to_string());
let result = if path.is_file() {
let findings = detector
.scan_file(path.to_str().context("Invalid path")?)
.map_err(|e| anyhow::anyhow!("Scan failed: {}", e))?;
let mut severity_counts = std::collections::HashMap::new();
for finding in &findings {
*severity_counts
.entry(finding.severity.to_string())
.or_insert(0) += 1;
}
security::injection::sql::ScanResult {
findings,
files_scanned: 1,
sinks_found: 0,
severity_counts,
language: lang_str.unwrap_or_else(|| "auto".to_string()),
}
} else {
detector
.scan_directory(path.to_str().context("Invalid path")?, lang_str.as_deref())
.map_err(|e| anyhow::anyhow!("Scan failed: {}", e))?
};
let min_sev = match min_severity.to_lowercase().as_str() {
"critical" => Severity::Critical,
"high" => Severity::High,
"medium" => Severity::Medium,
_ => Severity::Low,
};
let filtered_findings: Vec<_> = result
.findings
.into_iter()
.filter(|f| {
let sev_val = match f.severity {
Severity::Critical => 4,
Severity::High => 3,
Severity::Medium => 2,
Severity::Low => 1,
};
let min_val = match min_sev {
Severity::Critical => 4,
Severity::High => 3,
Severity::Medium => 2,
Severity::Low => 1,
};
sev_val >= min_val
})
.collect();
let filtered_result = security::injection::sql::ScanResult {
findings: filtered_findings,
files_scanned: result.files_scanned,
sinks_found: result.sinks_found,
severity_counts: result.severity_counts,
language: result.language,
};
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&filtered_result)
.context("Failed to serialize output")?
);
}
OutputFormat::Text | OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!("SQL Injection Scan Results");
println!("==========================");
println!();
println!("Files scanned: {}", filtered_result.files_scanned);
println!("Vulnerabilities found: {}", filtered_result.findings.len());
println!();
if filtered_result.findings.is_empty() {
println!("No SQL injection vulnerabilities detected.");
} else {
for (i, finding) in filtered_result.findings.iter().enumerate() {
println!("{}. [{}] {}:{}", i + 1, finding.severity, finding.location.file, finding.location.line);
println!(" Pattern: {}", finding.pattern);
println!(" Sink: {}", finding.sink_expression);
println!(" Code: {}", finding.code_snippet.lines().next().unwrap_or(""));
println!(" Description: {}", finding.description);
println!(" Remediation: {}", finding.remediation.lines().next().unwrap_or(""));
println!();
}
println!("Summary by Severity:");
for (severity, count) in &filtered_result.severity_counts {
println!(" {}: {}", severity, count);
}
}
}
}
Ok(())
}
fn cmd_command_injection(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
min_severity: &str,
min_confidence: &str,
) -> Result<()> {
use security::injection::command::{scan_command_injection, scan_file_command_injection, Severity, Confidence};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let findings = if path.is_file() {
scan_file_command_injection(path, lang_str.as_deref())
.map_err(|e| anyhow::anyhow!("Scan failed: {}", e))?
} else {
scan_command_injection(path, lang_str.as_deref())
.map_err(|e| anyhow::anyhow!("Scan failed: {}", e))?
};
let min_sev = match min_severity.to_lowercase().as_str() {
"critical" => Severity::Critical,
"high" => Severity::High,
"medium" => Severity::Medium,
"low" => Severity::Low,
_ => Severity::Info,
};
let min_conf = match min_confidence.to_lowercase().as_str() {
"high" => Confidence::High,
"medium" => Confidence::Medium,
_ => Confidence::Low,
};
let filtered_findings: Vec<_> = findings
.into_iter()
.filter(|f| f.severity >= min_sev && f.confidence >= min_conf)
.collect();
let mut severity_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for finding in &filtered_findings {
*severity_counts.entry(finding.severity.to_string()).or_insert(0) += 1;
}
let result = serde_json::json!({
"findings": filtered_findings,
"files_scanned": if path.is_file() { 1 } else {
filtered_findings.iter()
.map(|f| &f.location.file)
.collect::<std::collections::HashSet<_>>()
.len()
.max(1)
},
"vulnerabilities_found": filtered_findings.len(),
"severity_counts": severity_counts,
"language": lang_str.unwrap_or_else(|| "all".to_string()),
});
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
OutputFormat::Text | OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!("Command Injection Scan Results");
println!("==============================");
println!();
println!("Vulnerabilities found: {}", filtered_findings.len());
println!();
if filtered_findings.is_empty() {
println!("No command injection vulnerabilities detected.");
} else {
for (i, finding) in filtered_findings.iter().enumerate() {
println!(
"{}. [{}] [{}] {}:{}",
i + 1,
finding.severity,
finding.confidence,
finding.location.file,
finding.location.line
);
println!(" Sink: {}", finding.sink_function);
println!(" Type: {}", finding.kind);
println!(" Tainted Input: {}", finding.tainted_input);
if let Some(ref snippet) = finding.code_snippet {
let first_line = snippet.lines().next().unwrap_or("");
println!(" Code: {}", first_line);
}
println!(" Remediation: {}", finding.remediation.lines().next().unwrap_or(""));
println!();
}
println!("Summary by Severity:");
for (severity, count) in &severity_counts {
println!(" {}: {}", severity, count);
}
}
}
}
Ok(())
}
fn cmd_xss(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
min_severity: &str,
min_confidence: &str,
) -> Result<()> {
use security::injection::xss::{scan_xss, Severity, Confidence};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let result = scan_xss(path, lang_str.as_deref())
.map_err(|e| anyhow::anyhow!("Scan failed: {}", e))?;
let min_sev = match min_severity.to_lowercase().as_str() {
"critical" => Severity::Critical,
"high" => Severity::High,
"medium" => Severity::Medium,
"low" => Severity::Low,
_ => Severity::Info,
};
let min_conf = match min_confidence.to_lowercase().as_str() {
"high" => Confidence::High,
"medium" => Confidence::Medium,
_ => Confidence::Low,
};
let filtered_findings: Vec<_> = result
.findings
.into_iter()
.filter(|f| f.severity >= min_sev && f.confidence >= min_conf)
.collect();
let mut severity_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for finding in &filtered_findings {
*severity_counts.entry(finding.severity.to_string()).or_insert(0) += 1;
}
let output_result = serde_json::json!({
"findings": filtered_findings,
"files_scanned": result.files_scanned,
"vulnerabilities_found": filtered_findings.len(),
"severity_counts": severity_counts,
"language": lang_str.unwrap_or_else(|| "javascript/typescript".to_string()),
});
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&output_result).context("Failed to serialize output")?
);
}
OutputFormat::Text | OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!("XSS Vulnerability Scan Results");
println!("==============================");
println!();
println!("Files scanned: {}", result.files_scanned);
println!("Vulnerabilities found: {}", filtered_findings.len());
println!();
if filtered_findings.is_empty() {
println!("No XSS vulnerabilities detected.");
} else {
for (i, finding) in filtered_findings.iter().enumerate() {
println!(
"{}. [{}] [{}] {}:{}",
i + 1,
finding.severity,
finding.confidence,
finding.location.file,
finding.location.line
);
println!(" Sink Type: {}", finding.sink_type);
println!(" Context: {}", finding.context);
println!(" Sink: {}", finding.sink_expression.chars().take(80).collect::<String>());
if !finding.tainted_variables.is_empty() {
println!(" Tainted Variables: {}", finding.tainted_variables.join(", "));
}
if let Some(ref snippet) = finding.code_snippet {
let first_line = snippet.lines().next().unwrap_or("");
println!(" Code: {}", first_line);
}
println!(" Description: {}", finding.description.lines().next().unwrap_or(""));
println!(" Remediation: {}", finding.remediation.lines().next().unwrap_or(""));
println!();
}
println!("Summary by Severity:");
for (severity, count) in &severity_counts {
println!(" {}: {}", severity, count);
}
}
}
}
Ok(())
}
fn cmd_path_traversal(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
min_severity: &str,
min_confidence: &str,
) -> Result<()> {
use security::injection::path_traversal::{scan_path_traversal, scan_file_path_traversal, Severity, Confidence};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let findings = if path.is_file() {
scan_file_path_traversal(path, lang_str.as_deref())
.map_err(|e| anyhow::anyhow!("Scan failed: {}", e))?
} else {
scan_path_traversal(path, lang_str.as_deref())
.map_err(|e| anyhow::anyhow!("Scan failed: {}", e))?
};
let min_sev = match min_severity.to_lowercase().as_str() {
"critical" => Severity::Critical,
"high" => Severity::High,
"medium" => Severity::Medium,
"low" => Severity::Low,
_ => Severity::Info,
};
let min_conf = match min_confidence.to_lowercase().as_str() {
"high" => Confidence::High,
"medium" => Confidence::Medium,
_ => Confidence::Low,
};
let filtered_findings: Vec<_> = findings
.into_iter()
.filter(|f| f.severity >= min_sev && f.confidence >= min_conf)
.collect();
let mut severity_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for finding in &filtered_findings {
*severity_counts.entry(finding.severity.to_string()).or_insert(0) += 1;
}
let files_with_vulns: std::collections::HashSet<_> = filtered_findings
.iter()
.map(|f| &f.location.file)
.collect();
let result = serde_json::json!({
"findings": filtered_findings,
"files_scanned": if path.is_file() { 1 } else { files_with_vulns.len().max(1) },
"vulnerabilities_found": filtered_findings.len(),
"severity_counts": severity_counts,
"language": lang_str.unwrap_or_else(|| "all".to_string()),
});
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
OutputFormat::Text | OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!("Path Traversal Scan Results");
println!("===========================");
println!();
println!("Vulnerabilities found: {}", filtered_findings.len());
println!();
if filtered_findings.is_empty() {
println!("No path traversal vulnerabilities detected.");
} else {
for (i, finding) in filtered_findings.iter().enumerate() {
println!(
"{}. [{}] [{}] {}:{}",
i + 1,
finding.severity,
finding.confidence,
finding.location.file,
finding.location.line
);
println!(" Sink: {}", finding.sink_function);
println!(" Operation: {}", finding.operation_type);
println!(" Pattern: {}", finding.pattern);
println!(" Path Expression: {}",
finding.path_expression.chars().take(60).collect::<String>());
if !finding.involved_variables.is_empty() {
println!(" Variables: {}", finding.involved_variables.join(", "));
}
if finding.symlink_risk {
println!(" Symlink Risk: Yes");
}
if let Some(ref snippet) = finding.code_snippet {
let first_line = snippet.lines().next().unwrap_or("");
println!(" Code: {}", first_line);
}
println!(" Description: {}", finding.description.lines().next().unwrap_or(""));
println!(" Remediation: {}", finding.remediation.lines().next().unwrap_or(""));
println!();
}
println!("Summary by Severity:");
for (severity, count) in &severity_counts {
println!(" {}: {}", severity, count);
}
let symlink_count = filtered_findings.iter().filter(|f| f.symlink_risk).count();
if symlink_count > 0 {
println!();
println!("WARNING: {} findings may also be vulnerable to symlink attacks.", symlink_count);
println!("Consider using O_NOFOLLOW flag or equivalent protections.");
}
}
}
}
Ok(())
}
fn cmd_secrets(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
min_severity: &str,
min_confidence: &str,
include_entropy: bool,
) -> Result<()> {
use security::secrets::{SecretsDetector, Severity, Confidence};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let detector = SecretsDetector::new()
.include_entropy(include_entropy);
let result = if path.is_file() {
let findings = detector
.scan_file(path.to_str().context("Invalid path")?)
.map_err(|e| anyhow::anyhow!("Scan failed: {}", e))?;
let mut type_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let mut severity_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for finding in &findings {
*type_counts.entry(finding.secret_type.to_string()).or_insert(0) += 1;
*severity_counts.entry(finding.severity.to_string()).or_insert(0) += 1;
}
security::secrets::ScanResult {
findings,
files_scanned: 1,
type_counts,
severity_counts,
}
} else {
detector
.scan_directory(path.to_str().context("Invalid path")?, lang_str.as_deref())
.map_err(|e| anyhow::anyhow!("Scan failed: {}", e))?
};
let min_sev = match min_severity.to_lowercase().as_str() {
"critical" => Severity::Critical,
"high" => Severity::High,
"medium" => Severity::Medium,
"low" => Severity::Low,
_ => Severity::Info,
};
let min_conf = match min_confidence.to_lowercase().as_str() {
"high" => Confidence::High,
"medium" => Confidence::Medium,
_ => Confidence::Low,
};
let filtered_findings: Vec<_> = result
.findings
.into_iter()
.filter(|f| f.severity >= min_sev && f.confidence >= min_conf)
.collect();
let mut type_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let mut severity_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for finding in &filtered_findings {
*type_counts.entry(finding.secret_type.to_string()).or_insert(0) += 1;
*severity_counts.entry(finding.severity.to_string()).or_insert(0) += 1;
}
let filtered_result = security::secrets::ScanResult {
findings: filtered_findings.clone(),
files_scanned: result.files_scanned,
type_counts: type_counts.clone(),
severity_counts: severity_counts.clone(),
};
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&filtered_result).context("Failed to serialize output")?
);
}
OutputFormat::Text | OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!("Secrets Detection Scan Results");
println!("==============================");
println!();
println!("Files scanned: {}", filtered_result.files_scanned);
println!("Secrets found: {}", filtered_findings.len());
println!();
if filtered_findings.is_empty() {
println!("No hardcoded secrets detected.");
} else {
for (i, finding) in filtered_findings.iter().enumerate() {
println!(
"{}. [{}] [{}] {}:{}",
i + 1,
finding.severity,
finding.confidence,
finding.location.file,
finding.location.line
);
println!(" Type: {}", finding.secret_type);
println!(" Value: {}", finding.masked_value);
if let Some(ref var_name) = finding.variable_name {
println!(" Variable: {}", var_name);
}
if let Some(entropy) = finding.entropy {
println!(" Entropy: {:.2} bits/char", entropy);
}
if finding.is_test_file {
println!(" Test file: Yes (reduced severity)");
}
println!(" Description: {}", finding.description);
println!(" Remediation: {}", finding.remediation.lines().next().unwrap_or(""));
println!();
}
println!("Summary by Type:");
let mut type_vec: Vec<_> = type_counts.iter().collect();
type_vec.sort_by(|a, b| b.1.cmp(a.1));
for (secret_type, count) in type_vec {
println!(" {}: {}", secret_type, count);
}
println!();
println!("Summary by Severity:");
for (severity, count) in &severity_counts {
println!(" {}: {}", severity, count);
}
let critical_count = filtered_findings.iter().filter(|f| f.severity == Severity::Critical).count();
if critical_count > 0 {
println!();
println!("CRITICAL: {} secrets require immediate attention!", critical_count);
println!("Rotate credentials and remove from source control.");
}
}
}
}
Ok(())
}
fn cmd_crypto(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
min_severity: &str,
min_confidence: &str,
include_safe: bool,
) -> Result<()> {
use security::crypto::{WeakCryptoDetector, Severity, Confidence};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let detector = WeakCryptoDetector::new()
.include_safe_patterns(include_safe);
let result = if path.is_file() {
let findings = detector
.scan_file(path.to_str().context("Invalid path")?)
.map_err(|e| anyhow::anyhow!("Scan failed: {}", e))?;
let mut issue_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let mut severity_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let mut algorithm_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for finding in &findings {
*issue_counts.entry(finding.issue_type.to_string()).or_insert(0) += 1;
*severity_counts.entry(finding.severity.to_string()).or_insert(0) += 1;
*algorithm_counts.entry(finding.algorithm.to_string()).or_insert(0) += 1;
}
security::crypto::ScanResult {
findings,
files_scanned: 1,
issue_counts,
severity_counts,
algorithm_counts,
}
} else {
detector
.scan_directory(path.to_str().context("Invalid path")?, lang_str.as_deref())
.map_err(|e| anyhow::anyhow!("Scan failed: {}", e))?
};
let min_sev = match min_severity.to_lowercase().as_str() {
"critical" => Severity::Critical,
"high" => Severity::High,
"medium" => Severity::Medium,
"low" => Severity::Low,
_ => Severity::Info,
};
let min_conf = match min_confidence.to_lowercase().as_str() {
"high" => Confidence::High,
"medium" => Confidence::Medium,
_ => Confidence::Low,
};
let filtered_findings: Vec<_> = result
.findings
.into_iter()
.filter(|f| f.severity >= min_sev && f.confidence >= min_conf)
.collect();
let mut issue_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let mut severity_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let mut algorithm_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for finding in &filtered_findings {
*issue_counts.entry(finding.issue_type.to_string()).or_insert(0) += 1;
*severity_counts.entry(finding.severity.to_string()).or_insert(0) += 1;
*algorithm_counts.entry(finding.algorithm.to_string()).or_insert(0) += 1;
}
let filtered_result = security::crypto::ScanResult {
findings: filtered_findings.clone(),
files_scanned: result.files_scanned,
issue_counts: issue_counts.clone(),
severity_counts: severity_counts.clone(),
algorithm_counts: algorithm_counts.clone(),
};
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&filtered_result).context("Failed to serialize output")?
);
}
OutputFormat::Text | OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!("Weak Cryptography Detection Scan Results");
println!("=========================================");
println!();
println!("Files scanned: {}", filtered_result.files_scanned);
println!("Issues found: {}", filtered_findings.len());
println!();
if filtered_findings.is_empty() {
println!("No weak cryptography detected.");
} else {
for (i, finding) in filtered_findings.iter().enumerate() {
println!(
"{}. [{}] [{}] {}:{}",
i + 1,
finding.severity,
finding.confidence,
finding.location.file,
finding.location.line
);
println!(" Issue: {}", finding.issue_type);
println!(" Algorithm: {}", finding.algorithm);
println!(" Context: {:?}", finding.context);
if finding.likely_safe {
println!(" Likely Safe: Yes (checksum/cache usage)");
}
if finding.is_test_file {
println!(" Test file: Yes (reduced severity)");
}
println!(" Description: {}", finding.description);
println!(" Remediation: {}", finding.remediation.lines().next().unwrap_or(""));
println!();
}
println!("Summary by Issue Type:");
let mut issue_vec: Vec<_> = issue_counts.iter().collect();
issue_vec.sort_by(|a, b| b.1.cmp(a.1));
for (issue_type, count) in issue_vec {
println!(" {}: {}", issue_type, count);
}
println!();
println!("Summary by Algorithm:");
let mut algo_vec: Vec<_> = algorithm_counts.iter().collect();
algo_vec.sort_by(|a, b| b.1.cmp(a.1));
for (algorithm, count) in algo_vec {
println!(" {}: {}", algorithm, count);
}
println!();
println!("Summary by Severity:");
for (severity, count) in &severity_counts {
println!(" {}: {}", severity, count);
}
let critical_count = filtered_findings.iter().filter(|f| f.severity == Severity::Critical).count();
if critical_count > 0 {
println!();
println!("CRITICAL: {} cryptographic issues require immediate attention!", critical_count);
println!("Review hardcoded keys and weak algorithms.");
}
}
}
}
Ok(())
}
fn cmd_deserialization(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
min_severity: &str,
min_confidence: &str,
include_safe: bool,
) -> Result<()> {
use security::deserialization::{scan_deserialization, scan_file_deserialization, Severity, Confidence};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let findings = if path.is_file() {
scan_file_deserialization(path, lang_str.as_deref())
.map_err(|e| anyhow::anyhow!("Scan failed: {}", e))?
} else {
scan_deserialization(path, lang_str.as_deref())
.map_err(|e| anyhow::anyhow!("Scan failed: {}", e))?
};
let min_sev = match min_severity.to_lowercase().as_str() {
"critical" => Severity::Critical,
"high" => Severity::High,
"medium" => Severity::Medium,
"low" => Severity::Low,
_ => Severity::Info,
};
let min_conf = match min_confidence.to_lowercase().as_str() {
"high" => Confidence::High,
"medium" => Confidence::Medium,
_ => Confidence::Low,
};
let filtered_findings: Vec<_> = findings
.into_iter()
.filter(|f| {
f.severity >= min_sev &&
f.confidence >= min_conf &&
(include_safe || !f.possibly_safe)
})
.collect();
let mut severity_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let mut method_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let mut source_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for finding in &filtered_findings {
*severity_counts.entry(finding.severity.to_string()).or_insert(0) += 1;
*method_counts.entry(finding.method.to_string()).or_insert(0) += 1;
*source_counts.entry(finding.input_source.to_string()).or_insert(0) += 1;
}
let files_scanned = if path.is_file() {
1
} else {
filtered_findings
.iter()
.map(|f| &f.location.file)
.collect::<std::collections::HashSet<_>>()
.len()
.max(1)
};
let result = serde_json::json!({
"findings": filtered_findings,
"files_scanned": files_scanned,
"vulnerabilities_found": filtered_findings.len(),
"severity_counts": severity_counts,
"method_counts": method_counts,
"source_counts": source_counts,
"language": lang_str.unwrap_or_else(|| "all".to_string()),
});
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
OutputFormat::Text | OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!("Unsafe Deserialization Scan Results");
println!("====================================");
println!();
println!("Files scanned: {}", files_scanned);
println!("Vulnerabilities found: {}", filtered_findings.len());
println!();
if filtered_findings.is_empty() {
println!("No unsafe deserialization patterns detected.");
} else {
for (i, finding) in filtered_findings.iter().enumerate() {
println!(
"{}. [{}] [{}] {}:{}",
i + 1,
finding.severity,
finding.confidence,
finding.location.file,
finding.location.line
);
println!(" Method: {}", finding.method);
println!(" Function: {}", finding.sink_function);
println!(" Input Source: {}", finding.input_source);
println!(" Data: {}", finding.deserialized_data);
if finding.possibly_safe {
println!(" Status: POSSIBLY SAFE (review recommended)");
}
println!(" Description: {}", finding.description);
println!();
}
println!("Summary by Method:");
let mut method_vec: Vec<_> = method_counts.iter().collect();
method_vec.sort_by(|a, b| b.1.cmp(a.1));
for (method, count) in method_vec {
println!(" {}: {}", method, count);
}
println!();
println!("Summary by Input Source:");
let mut source_vec: Vec<_> = source_counts.iter().collect();
source_vec.sort_by(|a, b| b.1.cmp(a.1));
for (source, count) in source_vec {
println!(" {}: {}", source, count);
}
println!();
println!("Summary by Severity:");
for (severity, count) in &severity_counts {
println!(" {}: {}", severity, count);
}
let critical_count = filtered_findings.iter().filter(|f| f.severity == Severity::Critical).count();
if critical_count > 0 {
println!();
println!("CRITICAL: {} deserialization vulnerabilities allow RCE!", critical_count);
println!("These issues can execute arbitrary code without obvious sinks.");
println!("Review immediately and replace with safe alternatives.");
}
}
}
}
Ok(())
}
fn cmd_security_scan(
path: &PathBuf,
lang: Option<Language>,
format: SecurityOutputFormat,
min_severity: &str,
min_confidence: &str,
categories: Option<&str>,
fail_on: &str,
include_suppressed: bool,
max_files: usize,
) -> Result<i32> {
use security::{scan_security, SecurityConfig, Severity, Confidence};
require_exists(path)?;
let mut config = SecurityConfig::default();
config.language = lang.map(|l| l.to_string());
config.min_severity = match min_severity.to_lowercase().as_str() {
"critical" => Severity::Critical,
"high" => Severity::High,
"medium" => Severity::Medium,
"low" => Severity::Low,
_ => Severity::Info,
};
config.min_confidence = match min_confidence.to_lowercase().as_str() {
"high" => Confidence::High,
"medium" => Confidence::Medium,
_ => Confidence::Low,
};
if let Some(cats) = categories {
config.categories = Some(
cats.split(',')
.map(|s| s.trim().to_lowercase())
.collect()
);
}
config.include_suppressed = include_suppressed;
config.max_files = max_files;
config.deduplicate = true;
let report = scan_security(path, &config)
.map_err(|e| anyhow::anyhow!("Security scan failed: {}", e))?;
match format {
SecurityOutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&report).context("Failed to serialize output")?
);
}
SecurityOutputFormat::Sarif => {
let sarif_json = report
.to_sarif_json()
.map_err(|e| anyhow::anyhow!("Failed to generate SARIF: {}", e))?;
println!("{}", sarif_json);
}
SecurityOutputFormat::Text => {
println!("Security Scan Results");
println!("=====================");
println!();
println!("Scan Duration: {}ms", report.summary.scan_duration_ms);
println!("Files Scanned: {}", report.summary.files_scanned);
println!("Total Findings: {}", report.summary.total_findings);
println!("Duplicates Removed: {}", report.summary.duplicates_removed);
println!();
println!("By Severity:");
println!(" Critical: {}", report.summary.by_severity.get("Critical").unwrap_or(&0));
println!(" High: {}", report.summary.by_severity.get("High").unwrap_or(&0));
println!(" Medium: {}", report.summary.by_severity.get("Medium").unwrap_or(&0));
println!(" Low: {}", report.summary.by_severity.get("Low").unwrap_or(&0));
println!(" Info: {}", report.summary.by_severity.get("Info").unwrap_or(&0));
println!();
if !report.summary.by_category.is_empty() {
println!("By Category:");
for (category, count) in &report.summary.by_category {
println!(" {}: {}", category, count);
}
println!();
}
if report.findings.is_empty() {
println!("No security vulnerabilities detected.");
} else {
println!("Findings:");
println!("---------");
for (i, finding) in report.findings.iter().enumerate() {
let suppressed_marker = if finding.suppressed { " [SUPPRESSED]" } else { "" };
println!(
"{}. [{}] [{}] {}{}",
i + 1,
finding.id,
finding.severity,
finding.title,
suppressed_marker
);
println!(
" Location: {}:{}:{}",
finding.location.file, finding.location.start_line, finding.location.start_column
);
println!(" Category: {}", finding.category);
println!(" Confidence: {}", finding.confidence);
if let Some(cwe) = finding.cwe_id {
println!(" CWE: CWE-{}", cwe);
}
println!(" Description: {}", finding.description);
if !finding.remediation.is_empty() {
let first_line = finding.remediation.lines().next().unwrap_or("");
println!(" Remediation: {}", first_line);
}
if !finding.code_snippet.is_empty() {
let snippet = finding.code_snippet.lines().next().unwrap_or("");
if snippet.len() <= 80 {
println!(" Code: {}", snippet);
} else {
println!(" Code: {}...", &snippet[..77]);
}
}
println!();
}
}
}
}
let fail_severity = match fail_on.to_lowercase().as_str() {
"critical" => Severity::Critical,
"high" => Severity::High,
"medium" => Severity::Medium,
"low" => Severity::Low,
_ => Severity::High,
};
let has_severe_finding = report.findings.iter().any(|f| {
!f.suppressed && f.severity >= fail_severity
});
Ok(if has_severe_finding { 1 } else { 0 })
}
fn cmd_redos(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
min_severity: &str,
min_confidence: &str,
) -> Result<()> {
use security::redos::{scan_redos, Severity, Confidence};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let result = scan_redos(
path.to_string_lossy().as_ref(),
lang_str.as_deref(),
).map_err(|e| anyhow::anyhow!("Scan failed: {}", e))?;
let min_sev = match min_severity.to_lowercase().as_str() {
"critical" => Severity::Critical,
"high" => Severity::High,
"medium" => Severity::Medium,
"low" => Severity::Low,
_ => Severity::Info,
};
let min_conf = match min_confidence.to_lowercase().as_str() {
"high" => Confidence::High,
"medium" => Confidence::Medium,
_ => Confidence::Low,
};
let filtered_findings: Vec<_> = result
.findings
.into_iter()
.filter(|f| f.severity >= min_sev && f.confidence >= min_conf)
.collect();
let mut severity_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let mut vuln_type_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for finding in &filtered_findings {
*severity_counts.entry(finding.severity.to_string()).or_insert(0) += 1;
*vuln_type_counts.entry(finding.vulnerability_type.to_string()).or_insert(0) += 1;
}
let output_result = serde_json::json!({
"findings": filtered_findings,
"files_scanned": result.files_scanned,
"vulnerabilities_found": filtered_findings.len(),
"severity_counts": severity_counts,
"vulnerability_type_counts": vuln_type_counts,
"language": lang_str.unwrap_or_else(|| "all".to_string()),
});
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&output_result).context("Failed to serialize output")?
);
}
OutputFormat::Text | OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!("ReDoS (Regular Expression Denial of Service) Scan Results");
println!("==========================================================");
println!();
println!("Files scanned: {}", result.files_scanned);
println!("Vulnerabilities found: {}", filtered_findings.len());
println!();
if filtered_findings.is_empty() {
println!("No ReDoS vulnerabilities detected.");
} else {
for (i, finding) in filtered_findings.iter().enumerate() {
println!(
"{}. [{}] [{}] {}:{}",
i + 1,
finding.severity,
finding.confidence,
finding.location.file,
finding.location.line
);
println!(" Type: {}", finding.vulnerability_type);
println!(" Function: {}", finding.regex_function);
println!(" Pattern: {}", finding.regex_pattern);
println!(" Complexity: {}", finding.complexity);
if !finding.attack_string.is_empty() {
println!(" Attack String: {}", finding.attack_string);
}
println!(" Description: {}", finding.description);
println!();
}
println!("Summary by Vulnerability Type:");
let mut vuln_vec: Vec<_> = vuln_type_counts.iter().collect();
vuln_vec.sort_by(|a, b| b.1.cmp(a.1));
for (vuln_type, count) in vuln_vec {
println!(" {}: {}", vuln_type, count);
}
println!();
println!("Summary by Severity:");
for (severity, count) in &severity_counts {
println!(" {}: {}", severity, count);
}
}
}
}
Ok(())
}
fn cmd_metrics(subcmd: MetricsCommands) -> Result<()> {
match subcmd {
MetricsCommands::Complexity {
path,
lang,
format,
threshold,
sort,
violations_only,
} => {
cmd_complexity(&path, lang, format, threshold, sort, violations_only)?;
}
MetricsCommands::Cognitive {
path,
lang,
format,
threshold,
sort,
violations_only,
breakdown,
} => {
cmd_cognitive(&path, lang, format, threshold, sort, violations_only, breakdown)?;
}
MetricsCommands::Halstead {
path,
lang,
format,
sort,
sort_by_difficulty,
show_tokens,
} => {
cmd_halstead(&path, lang, format, sort, sort_by_difficulty, show_tokens)?;
}
MetricsCommands::Maintainability {
path,
lang,
format,
threshold,
sort,
violations_only,
include_comments,
} => {
cmd_maintainability(&path, lang, format, threshold, sort, violations_only, include_comments)?;
}
MetricsCommands::Loc {
path,
lang,
format,
by_language,
sort,
function_threshold,
violations_only,
top,
} => {
cmd_loc(&path, lang, format, by_language, sort, function_threshold, violations_only, top)?;
}
MetricsCommands::Nesting {
path,
lang,
format,
threshold,
sort,
violations_only,
details,
} => {
cmd_nesting(&path, lang, format, threshold, sort, violations_only, details)?;
}
MetricsCommands::Functions {
path,
lang,
format,
sort_by,
violations_only,
sloc_warn,
sloc_critical,
params_warn,
params_critical,
details,
} => {
cmd_function_size(
&path,
lang,
format,
sort_by,
violations_only,
sloc_warn,
sloc_critical,
params_warn,
params_critical,
details,
)?;
}
MetricsCommands::Coupling {
path,
lang,
format,
level,
sort,
threshold,
show_cycles,
show_edges,
} => {
cmd_coupling(&path, lang, format, level, sort, threshold, show_cycles, show_edges)?;
}
MetricsCommands::Cohesion {
path,
lang,
format,
threshold,
sort,
violations_only,
show_components,
} => {
cmd_cohesion(&path, lang, format, threshold, sort, violations_only, show_components)?;
}
MetricsCommands::Report {
path,
lang,
format,
thresholds,
sort_by,
issues_only,
skip_coupling,
fail_on,
max_files,
top,
show_tokens,
} => {
let exit_code = cmd_metrics_report(
&path,
lang,
format,
&thresholds,
sort_by,
issues_only,
skip_coupling,
fail_on.as_deref(),
max_files,
top,
show_tokens,
)?;
if exit_code != 0 {
std::process::exit(exit_code);
}
}
}
Ok(())
}
fn cmd_complexity(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
threshold: Option<u32>,
sort: bool,
violations_only: bool,
) -> Result<()> {
use metrics::{analyze_complexity, ComplexityAnalysis, RiskLevel};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let mut result = analyze_complexity(path, lang_str.as_deref(), threshold)
.map_err(|e| anyhow::anyhow!("Complexity analysis failed: {}", e))?;
if sort {
result.functions.sort_by(|a, b| b.complexity.cmp(&a.complexity));
if let Some(ref mut violations) = result.violations {
violations.sort_by(|a, b| b.complexity.cmp(&a.complexity));
}
}
let output = if violations_only && result.violations.is_some() {
serde_json::json!({
"path": result.path,
"language": result.language,
"threshold": result.threshold,
"violations": result.violations,
"stats": result.stats,
})
} else {
serde_json::to_value(&result).context("Failed to serialize result")?
};
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("Cyclomatic Complexity Analysis");
println!("==============================");
println!("Path: {}", result.path.display());
if let Some(ref lang) = result.language {
println!("Language: {}", lang);
}
println!();
println!("Statistics:");
println!(" Total functions: {}", result.stats.total_functions);
println!(" Average complexity: {:.2}", result.stats.average_complexity);
println!(" Min complexity: {}", result.stats.min_complexity);
println!(" Max complexity: {}", result.stats.max_complexity);
println!(" Median complexity: {}", result.stats.median_complexity);
println!();
println!("Risk Distribution:");
for (risk, count) in &result.stats.risk_distribution {
let level = match risk.as_str() {
"low" => RiskLevel::Low,
"medium" => RiskLevel::Medium,
"high" => RiskLevel::High,
"critical" => RiskLevel::Critical,
_ => RiskLevel::Low,
};
println!(" {}{}: {}\x1b[0m", level.color_code(), risk, count);
}
println!();
let functions_to_show = if violations_only && result.violations.is_some() {
result.violations.as_ref().unwrap()
} else {
&result.functions
};
if !functions_to_show.is_empty() {
let header = if violations_only {
format!("Violations (complexity > {}):", threshold.unwrap_or(0))
} else {
"Functions:".to_string()
};
println!("{}", header);
for func in functions_to_show {
let level = func.risk_level;
println!(
" {}{}: {} ({}) - {}:{}\x1b[0m",
level.color_code(),
func.function_name,
func.complexity,
func.risk_level,
func.file.display(),
func.line
);
}
}
if !result.stats.histogram.is_empty() {
println!();
println!("Complexity Histogram:");
let max_count = result.stats.histogram.iter().map(|b| b.count).max().unwrap_or(1);
for bucket in &result.stats.histogram {
if bucket.count > 0 {
let bar_len = (bucket.count as f64 / max_count as f64 * 40.0) as usize;
let bar = "#".repeat(bar_len.max(1));
println!(" {:>6}: {} ({})", bucket.label, bar, bucket.count);
}
}
}
}
OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
}
if let Some(t) = threshold {
if let Some(ref violations) = result.violations {
let count = violations.len();
if count > 0 {
eprintln!();
eprintln!(
"WARNING: {} function(s) exceed complexity threshold of {}",
count, t
);
}
}
}
Ok(())
}
fn cmd_cognitive(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
threshold: Option<u32>,
sort: bool,
violations_only: bool,
show_breakdown: bool,
) -> Result<()> {
use metrics::{analyze_cognitive_complexity, CognitiveRiskLevel};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let mut result = analyze_cognitive_complexity(path, lang_str.as_deref(), threshold)
.map_err(|e| anyhow::anyhow!("Cognitive complexity analysis failed: {}", e))?;
if sort {
result.functions.sort_by(|a, b| b.complexity.cmp(&a.complexity));
if let Some(ref mut violations) = result.violations {
violations.sort_by(|a, b| b.complexity.cmp(&a.complexity));
}
}
let output = if violations_only && result.violations.is_some() {
serde_json::json!({
"path": result.path,
"language": result.language,
"threshold": result.threshold,
"violations": result.violations,
"stats": result.stats,
})
} else {
serde_json::to_value(&result).context("Failed to serialize result")?
};
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("Cognitive Complexity Analysis (SonarSource)");
println!("============================================");
println!("Path: {}", result.path.display());
if let Some(ref lang) = result.language {
println!("Language: {}", lang);
}
println!();
println!("Statistics:");
println!(" Total functions: {}", result.stats.total_functions);
println!(" Average complexity: {:.2}", result.stats.average_complexity);
println!(" Min complexity: {}", result.stats.min_complexity);
println!(" Max complexity: {}", result.stats.max_complexity);
println!(" Median complexity: {}", result.stats.median_complexity);
println!(" Average max nesting: {:.2}", result.stats.average_max_nesting);
println!(" Functions with recursion: {}", result.stats.functions_with_recursion);
println!();
println!("Risk Distribution:");
for (risk, count) in &result.stats.risk_distribution {
let level = match risk.as_str() {
"low" => CognitiveRiskLevel::Low,
"medium" => CognitiveRiskLevel::Medium,
"high" => CognitiveRiskLevel::High,
"critical" => CognitiveRiskLevel::Critical,
_ => CognitiveRiskLevel::Low,
};
println!(" {}{}: {}\x1b[0m", level.color_code(), risk, count);
}
println!();
let functions_to_show = if violations_only && result.violations.is_some() {
result.violations.as_ref().unwrap()
} else {
&result.functions
};
if !functions_to_show.is_empty() {
let header = if violations_only {
format!("Violations (complexity > {}):", threshold.unwrap_or(0))
} else {
"Functions:".to_string()
};
println!("{}", header);
for func in functions_to_show {
let level = func.risk_level;
println!(
" {}{}: {} ({}) - {}:{} [max nesting: {}]\x1b[0m",
level.color_code(),
func.function_name,
func.complexity,
func.risk_level,
func.file.display(),
func.line,
func.max_nesting
);
if show_breakdown && !func.breakdown.is_empty() {
for contrib in &func.breakdown {
let increment_desc = if contrib.nesting_increment > 0 {
format!("+{} (base) +{} (nesting)", contrib.base_increment, contrib.nesting_increment)
} else {
format!("+{}", contrib.base_increment)
};
println!(
" L{}: {} {}",
contrib.line,
contrib.construct,
increment_desc
);
}
}
}
}
}
OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
}
if let Some(t) = threshold {
if let Some(ref violations) = result.violations {
let count = violations.len();
if count > 0 {
eprintln!();
eprintln!(
"WARNING: {} function(s) exceed cognitive complexity threshold of {}",
count, t
);
}
}
}
Ok(())
}
fn cmd_halstead(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
sort: bool,
sort_by_difficulty: bool,
show_tokens: bool,
) -> Result<()> {
use metrics::{analyze_halstead, QualityLevel};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let mut result = analyze_halstead(path, lang_str.as_deref(), show_tokens)
.map_err(|e| anyhow::anyhow!("Halstead analysis failed: {}", e))?;
if sort || sort_by_difficulty {
if sort_by_difficulty {
result.functions.sort_by(|a, b| {
b.metrics.difficulty
.partial_cmp(&a.metrics.difficulty)
.unwrap_or(std::cmp::Ordering::Equal)
});
} else {
result.functions.sort_by(|a, b| {
b.metrics.volume
.partial_cmp(&a.metrics.volume)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
}
match format {
OutputFormat::Json => {
let output = serde_json::to_value(&result).context("Failed to serialize result")?;
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("Halstead Complexity Analysis");
println!("============================");
println!("Path: {}", result.path.display());
if let Some(ref lang) = result.language {
println!("Language: {}", lang);
}
println!();
println!("Aggregate Statistics:");
println!(" Total functions: {}", result.stats.total_functions);
println!(" Average volume: {:.1}", result.stats.avg_volume);
println!(" Max volume: {:.1}", result.stats.max_volume);
println!(" Average difficulty: {:.1}", result.stats.avg_difficulty);
println!(" Max difficulty: {:.1}", result.stats.max_difficulty);
println!(" Total estimated bugs: {:.2}", result.stats.total_bugs);
println!(" Total estimated time: {:.0}s ({:.1}h)",
result.stats.total_time_seconds,
result.stats.total_time_seconds / 3600.0
);
println!();
if !result.functions.is_empty() {
println!("Functions:");
for func in &result.functions {
let color = match func.quality.volume_level {
QualityLevel::Low => "\x1b[32m", QualityLevel::Medium => "\x1b[33m", QualityLevel::High => "\x1b[31m", QualityLevel::VeryHigh => "\x1b[35m", };
println!(
" {}{}: V={:.1}, D={:.1}, E={:.0}, T={:.0}s, B={:.3}\x1b[0m",
color,
func.function_name,
func.metrics.volume,
func.metrics.difficulty,
func.metrics.effort,
func.metrics.time_seconds,
func.metrics.bugs
);
println!(
" n1={}, n2={}, N1={}, N2={} - {}:{}",
func.metrics.distinct_operators,
func.metrics.distinct_operands,
func.metrics.total_operators,
func.metrics.total_operands,
func.file.display(),
func.line
);
if show_tokens {
if let Some(ref operators) = func.operators {
let ops_preview: Vec<_> = operators.iter().take(10).collect();
let suffix = if operators.len() > 10 {
format!("... (+{} more)", operators.len() - 10)
} else {
String::new()
};
println!(" Operators: {:?}{}", ops_preview, suffix);
}
if let Some(ref operands) = func.operands {
let ops_preview: Vec<_> = operands.iter().take(10).collect();
let suffix = if operands.len() > 10 {
format!("... (+{} more)", operands.len() - 10)
} else {
String::new()
};
println!(" Operands: {:?}{}", ops_preview, suffix);
}
}
}
}
if !result.errors.is_empty() {
println!();
println!("Errors ({}):", result.errors.len());
for err in &result.errors {
println!(" {}: {}", err.file.display(), err.message);
}
}
}
OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
let output = serde_json::to_value(&result).context("Failed to serialize result")?;
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
}
let high_complexity_count = result.functions.iter()
.filter(|f| matches!(f.quality.difficulty_level, QualityLevel::High | QualityLevel::VeryHigh))
.count();
if high_complexity_count > 0 {
eprintln!();
eprintln!(
"WARNING: {} function(s) have high difficulty (>15), consider refactoring",
high_complexity_count
);
}
Ok(())
}
fn cmd_maintainability(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
threshold: Option<f64>,
sort: bool,
violations_only: bool,
include_comments: bool,
) -> Result<()> {
use metrics::{analyze_maintainability, MaintainabilityRiskLevel};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let mut result = analyze_maintainability(path, lang_str.as_deref(), threshold, include_comments)
.map_err(|e| anyhow::anyhow!("Maintainability analysis failed: {}", e))?;
if sort {
result.functions.sort_by(|a, b| {
a.index.score
.partial_cmp(&b.index.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
if let Some(ref mut violations) = result.violations {
violations.sort_by(|a, b| {
a.index.score
.partial_cmp(&b.index.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
}
let output = if violations_only && result.violations.is_some() {
serde_json::json!({
"path": result.path,
"language": result.language,
"threshold": result.threshold,
"violations": result.violations,
"stats": result.stats,
"includes_comments": result.includes_comments,
})
} else {
serde_json::to_value(&result).context("Failed to serialize result")?
};
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("Maintainability Index Analysis");
println!("===============================");
println!("Path: {}", result.path.display());
if let Some(ref lang) = result.language {
println!("Language: {}", lang);
}
if result.includes_comments {
println!("Formula: Extended (with comment bonus)");
} else {
println!("Formula: Standard (Visual Studio)");
}
println!();
println!("Statistics:");
println!(" Total functions: {}", result.stats.total_functions);
println!(" Average MI: {:.1}", result.stats.average_mi);
println!(" Min MI: {:.1}", result.stats.min_mi);
println!(" Max MI: {:.1}", result.stats.max_mi);
println!(" Median MI: {:.1}", result.stats.median_mi);
println!(" Total SLOC: {}", result.stats.total_sloc);
println!(" Total comment lines: {}", result.stats.total_comment_lines);
println!(" Overall comment %: {:.1}%", result.stats.overall_comment_percentage);
println!(" Average Halstead Volume: {:.1}", result.stats.average_volume);
println!(" Average Cyclomatic Complexity: {:.1}", result.stats.average_cc);
println!();
println!("Risk Distribution:");
for (risk, count) in &result.stats.risk_distribution {
let level = match risk.as_str() {
"low" => MaintainabilityRiskLevel::Low,
"medium" => MaintainabilityRiskLevel::Medium,
"high" => MaintainabilityRiskLevel::High,
"critical" => MaintainabilityRiskLevel::Critical,
_ => MaintainabilityRiskLevel::Medium,
};
println!(" {}{}: {}\x1b[0m", level.color_code(), risk, count);
}
println!();
let functions_to_show = if violations_only && result.violations.is_some() {
result.violations.as_ref().unwrap()
} else {
&result.functions
};
if !functions_to_show.is_empty() {
let header = if violations_only {
format!("Violations (MI < {}):", threshold.unwrap_or(0.0))
} else {
"Functions:".to_string()
};
println!("{}", header);
for func in functions_to_show {
let level = func.index.risk_level;
println!(
" {}{}: MI={:.1} ({}) - {}:{}\x1b[0m",
level.color_code(),
func.function_name,
func.index.score,
func.index.risk_level,
func.file.display(),
func.line
);
println!(
" V={:.1}, CC={}, SLOC={}, Comments={:.1}%",
func.index.halstead_volume,
func.index.cyclomatic_complexity,
func.index.lines_of_code.effective,
func.index.comment_percentage
);
}
}
if !result.errors.is_empty() {
println!();
println!("Errors ({}):", result.errors.len());
for err in &result.errors {
println!(" {}: {}", err.file.display(), err.message);
}
}
}
OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
let output = serde_json::to_value(&result).context("Failed to serialize result")?;
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
}
if let Some(t) = threshold {
if let Some(ref violations) = result.violations {
let count = violations.len();
if count > 0 {
eprintln!();
eprintln!(
"WARNING: {} function(s) have Maintainability Index below {} (hard to maintain)",
count, t
);
}
}
}
let critical_count = result.functions.iter()
.filter(|f| matches!(f.index.risk_level, MaintainabilityRiskLevel::Critical))
.count();
if critical_count > 0 {
eprintln!();
eprintln!(
"CRITICAL: {} function(s) have MI < 10 (very hard to maintain, refactor immediately)",
critical_count
);
}
Ok(())
}
fn cmd_loc(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
by_language: bool,
sort: bool,
function_threshold: u32,
violations_only: bool,
top: usize,
) -> Result<()> {
use metrics::{analyze_loc, LOCAnalysis};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let mut result = analyze_loc(path, lang_str.as_deref(), Some(function_threshold))
.map_err(|e| anyhow::anyhow!("LOC analysis failed: {}", e))?;
if sort {
result.files.sort_by(|a, b| b.metrics.source.cmp(&a.metrics.source));
}
let output = if violations_only {
serde_json::json!({
"path": result.path,
"language": result.language,
"function_threshold": function_threshold,
"oversized_functions": result.oversized_functions,
"stats": {
"total_functions": result.stats.total_functions,
"oversized_function_count": result.stats.oversized_function_count,
},
})
} else if by_language {
serde_json::json!({
"path": result.path,
"by_language": result.by_language,
"stats": result.stats,
})
} else {
serde_json::to_value(&result).context("Failed to serialize result")?
};
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("Lines of Code Analysis");
println!("======================");
println!("Path: {}", result.path.display());
if let Some(ref lang) = result.language {
println!("Language: {}", lang);
}
println!();
println!("Statistics:");
println!(" Total SLOC: {}", result.stats.total_sloc);
println!(" Total physical lines: {}", result.stats.total_physical);
println!(" Total logical lines: {}", result.stats.total_logical);
println!(" Comment lines: {}", result.stats.total_comment);
println!(" Blank lines: {}", result.stats.total_blank);
println!(" Code-to-comment ratio: {:.2}", result.stats.code_to_comment_ratio);
println!(" Blank ratio: {:.1}%", result.stats.blank_ratio);
println!(" Files analyzed: {}", result.files.len());
println!(" Average SLOC/file: {:.1}", result.stats.avg_sloc_per_file);
println!(" Max SLOC: {}", result.stats.max_sloc);
println!(" Min SLOC: {}", result.stats.min_sloc);
println!(" Median SLOC: {}", result.stats.median_sloc);
println!();
println!("Function Statistics:");
println!(" Total functions: {}", result.stats.total_functions);
println!(" Average function size: {:.1} SLOC", result.stats.avg_function_size);
println!(" Oversized functions (>{} SLOC): {}",
function_threshold, result.stats.oversized_function_count);
println!();
if by_language && !result.by_language.is_empty() {
println!("By Language:");
for lang_stats in &result.by_language {
println!(
" {}: {} files, {} SLOC, {:.1} avg SLOC/file",
lang_stats.language,
lang_stats.file_count,
lang_stats.metrics.source,
lang_stats.avg_sloc_per_file
);
}
println!();
}
if !result.largest_files.is_empty() {
println!("Largest Files (top {}):", top.min(result.largest_files.len()));
for (i, file) in result.largest_files.iter().take(top).enumerate() {
println!(
" {}. {} - {} SLOC ({:.1}%)",
i + 1,
file.file.display(),
file.sloc,
file.percentage
);
}
println!();
}
if !result.oversized_functions.is_empty() {
println!("Oversized Functions (>{} SLOC):", function_threshold);
for func in &result.oversized_functions {
println!(
" \x1b[33m{}: {} SLOC - {}:{}\x1b[0m",
func.name,
func.sloc,
func.file.display(),
func.line
);
}
}
if !result.errors.is_empty() {
println!();
println!("Errors ({}):", result.errors.len());
for err in &result.errors {
println!(" {}: {}", err.file.display(), err.message);
}
}
}
OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
let output = serde_json::to_value(&result).context("Failed to serialize result")?;
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
}
if result.stats.oversized_function_count > 0 {
eprintln!();
eprintln!(
"WARNING: {} function(s) exceed {} SLOC threshold (consider splitting)",
result.stats.oversized_function_count, function_threshold
);
}
Ok(())
}
fn cmd_nesting(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
threshold: Option<u32>,
sort: bool,
violations_only: bool,
details: bool,
) -> Result<()> {
use metrics::{analyze_nesting, NestingDepthLevel};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let mut result = analyze_nesting(path, lang_str.as_deref(), threshold)
.map_err(|e| anyhow::anyhow!("Nesting analysis failed: {}", e))?;
if sort {
result.functions.sort_by(|a, b| b.max_depth.cmp(&a.max_depth));
if let Some(ref mut violations) = result.violations {
violations.sort_by(|a, b| b.max_depth.cmp(&a.max_depth));
}
}
let output = if violations_only && result.violations.is_some() {
serde_json::json!({
"path": result.path,
"language": result.language,
"threshold": result.threshold,
"violations": result.violations,
"stats": result.stats,
})
} else {
serde_json::to_value(&result).context("Failed to serialize result")?
};
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("Nesting Depth Analysis");
println!("======================");
println!("Path: {}", result.path.display());
if let Some(ref lang) = result.language {
println!("Language: {}", lang);
}
if let Some(t) = result.threshold {
println!("Threshold: {}", t);
}
println!();
println!("Statistics:");
println!(" Total functions: {}", result.stats.total_functions);
println!(" Average max depth: {:.2}", result.stats.average_max_depth);
println!(" Global max depth: {}", result.stats.global_max_depth);
println!(" Min max depth: {}", result.stats.min_max_depth);
println!(" Median max depth: {}", result.stats.median_max_depth);
println!(
" Functions over threshold: {}",
result.stats.functions_over_threshold
);
println!();
println!("Risk Distribution:");
for (risk, count) in &result.stats.risk_distribution {
let level = match risk.as_str() {
"good" => NestingDepthLevel::Good,
"acceptable" => NestingDepthLevel::Acceptable,
"complex" => NestingDepthLevel::Complex,
"severe" => NestingDepthLevel::Severe,
_ => NestingDepthLevel::Good,
};
println!(" {}{}: {}\x1b[0m", level.color_code(), risk, count);
}
println!();
let functions_to_show = if violations_only && result.violations.is_some() {
result.violations.as_ref().unwrap()
} else {
&result.functions
};
if !functions_to_show.is_empty() {
let header = if violations_only {
format!("Violations (depth > {}):", threshold.unwrap_or(5))
} else {
"Functions:".to_string()
};
println!("{}", header);
for func in functions_to_show {
let level = func.risk_level;
println!(
" {}{}: max_depth={} avg={:.1} ({}) - {}:{}\x1b[0m",
level.color_code(),
func.function_name,
func.max_depth,
func.average_depth,
func.risk_level,
func.file.display(),
func.line
);
if details {
if func.max_depth > 0 {
println!(" Deepest at line: {}", func.deepest_line);
}
if !func.deep_locations.is_empty() {
println!(" Deep nesting locations:");
for loc in &func.deep_locations {
println!(
" Line {}: depth {} [{}]",
loc.line,
loc.depth,
loc.construct_stack.join(" > ")
);
}
}
if !func.suggestions.is_empty() {
println!(" Suggestions:");
for suggestion in &func.suggestions {
println!(" - {}", suggestion);
}
}
if !func.depth_distribution.is_empty() {
let mut depths: Vec<_> = func.depth_distribution.iter().collect();
depths.sort_by_key(|&(d, _)| d);
print!(" Depth distribution: ");
for (depth, count) in depths {
print!("d{}={} ", depth, count);
}
println!();
}
println!();
}
}
}
}
OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
}
if let Some(t) = threshold {
if let Some(ref violations) = result.violations {
let count = violations.len();
if count > 0 {
eprintln!();
eprintln!(
"WARNING: {} function(s) have nesting depth > {} (consider refactoring)",
count, t
);
}
}
}
Ok(())
}
fn cmd_function_size(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
sort_by: Option<String>,
violations_only: bool,
sloc_warn: u32,
sloc_critical: u32,
params_warn: u32,
params_critical: u32,
details: bool,
) -> Result<()> {
use metrics::{
analyze_function_size, SizeThresholds, SizeSortBy,
sort_functions,
};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let thresholds = SizeThresholds {
sloc_warning: sloc_warn,
sloc_critical,
params_warning: params_warn,
params_critical,
..SizeThresholds::default()
};
let mut result = analyze_function_size(path, lang_str.as_deref(), Some(thresholds.clone()))
.map_err(|e| anyhow::anyhow!("Function size analysis failed: {}", e))?;
if let Some(ref sort_field) = sort_by {
let sort_option = sort_field.parse::<SizeSortBy>().unwrap_or(SizeSortBy::Sloc);
sort_functions(&mut result.functions, sort_option, true); sort_functions(&mut result.violations, sort_option, true);
}
let output = if violations_only {
serde_json::json!({
"path": result.path,
"language": result.language,
"violations": result.violations,
"stats": result.stats,
"thresholds": result.thresholds,
})
} else {
serde_json::to_value(&result).context("Failed to serialize result")?
};
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("Function Size Analysis");
println!("======================");
println!("Path: {}", result.path.display());
if let Some(ref lang) = result.language {
println!("Language: {}", lang);
}
println!();
println!("Statistics:");
println!(" Total functions: {}", result.stats.total_functions);
println!(
" Functions with issues: {} ({:.1}%)",
result.stats.functions_with_issues,
if result.stats.total_functions > 0 {
result.stats.functions_with_issues as f64 / result.stats.total_functions as f64 * 100.0
} else {
0.0
}
);
println!(
" Critical issues: {}",
result.stats.critical_issues
);
println!(
" Warning issues: {}",
result.stats.warning_issues
);
println!(" Average SLOC: {:.1}", result.stats.avg_sloc);
println!(" Max SLOC: {}", result.stats.max_sloc);
println!(" Average parameters: {:.1}", result.stats.avg_parameters);
println!(" Max parameters: {}", result.stats.max_parameters);
println!();
println!("Thresholds:");
println!(
" SLOC: warning > {}, critical > {}",
thresholds.sloc_warning, thresholds.sloc_critical
);
println!(
" Parameters: warning > {}, critical > {}",
thresholds.params_warning, thresholds.params_critical
);
println!(
" Variables: warning > {}, critical > {}",
thresholds.variables_warning, thresholds.variables_critical
);
println!(" Returns: warning > {}", thresholds.returns_warning);
println!();
if !result.stats.issue_counts.is_empty() {
println!("Issue Breakdown:");
for (issue_type, count) in &result.stats.issue_counts {
println!(" {}: {}", issue_type.replace('_', " "), count);
}
println!();
}
if !result.stats.category_distribution.is_empty() {
println!("Function Categories:");
for (category, count) in &result.stats.category_distribution {
println!(" {}: {}", category, count);
}
println!();
}
let functions_to_show = if violations_only {
&result.violations
} else {
&result.functions
};
if !functions_to_show.is_empty() {
let header = if violations_only {
"Functions with Issues:".to_string()
} else {
"All Functions:".to_string()
};
println!("{}", header);
for func in functions_to_show {
let severity = func.max_severity();
let color = severity
.map(|s| s.color_code())
.unwrap_or("\x1b[32m");
let issue_indicator = if func.issues.is_empty() {
"OK".to_string()
} else {
format!("{} issue(s)", func.issues.len())
};
println!(
" {}{}: sloc={} params={} vars={} returns={} branches={} [{}] - {}:{}\x1b[0m",
color,
func.name,
func.sloc,
func.parameters,
func.local_variables,
func.return_statements,
func.branches,
issue_indicator,
func.file.display(),
func.line
);
if details {
println!(" Category: {}", func.category);
println!(" Size score: {:.1}", func.size_score());
if !func.issues.is_empty() {
println!(" Issues:");
for issue in &func.issues {
let sev_color = issue.severity().color_code();
println!(
" {}{}: {}\x1b[0m",
sev_color,
issue.severity(),
issue.description()
);
}
}
if !func.suggestions.is_empty() {
println!(" Suggestions:");
for suggestion in &func.suggestions {
println!(" - {}", suggestion);
}
}
println!();
}
}
}
}
OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
}
let total_issues = result.stats.critical_issues + result.stats.warning_issues;
if total_issues > 0 {
eprintln!();
eprintln!(
"WARNING: {} function(s) with {} issue(s) detected (consider refactoring)",
result.stats.functions_with_issues,
total_issues
);
if result.stats.critical_issues > 0 {
eprintln!(
" {} critical issue(s) require immediate attention",
result.stats.critical_issues
);
}
}
Ok(())
}
fn cmd_coupling(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
level: CouplingLevelArg,
sort: bool,
threshold: Option<f64>,
show_cycles: bool,
show_edges: bool,
) -> Result<()> {
use metrics::{analyze_coupling, CouplingLevel, CouplingRisk};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let coupling_level: CouplingLevel = level.into();
let mut result = analyze_coupling(path, lang_str.as_deref(), coupling_level)
.map_err(|e| anyhow::anyhow!("Coupling analysis failed: {}", e))?;
if !sort {
result.modules.sort_by(|a, b| a.module.cmp(&b.module));
}
if let Some(t) = threshold {
result.modules.retain(|m| m.distance >= t);
}
let mut output = serde_json::to_value(&result).context("Failed to serialize result")?;
if !show_edges {
if let Some(obj) = output.as_object_mut() {
obj.remove("edges");
}
}
if !show_cycles {
if let Some(obj) = output.as_object_mut() {
obj.remove("circular_dependencies");
}
}
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("Coupling Analysis");
println!("=================");
println!("Path: {}", result.path.display());
if let Some(ref lang) = result.language {
println!("Language: {}", lang);
}
println!("Level: {}", result.level);
println!();
println!("Statistics:");
println!(" Total modules: {}", result.stats.total_modules);
println!(" Total dependencies: {}", result.stats.total_dependencies);
println!(" Average afferent (Ca): {:.2}", result.stats.avg_afferent);
println!(" Average efferent (Ce): {:.2}", result.stats.avg_efferent);
println!(" Average instability (I): {:.2}", result.stats.avg_instability);
println!(" Average distance (D): {:.2}", result.stats.avg_distance);
println!();
if result.stats.zone_of_pain_count > 0 {
println!(
" \x1b[31mZone of Pain: {} module(s)\x1b[0m (stable, concrete - rigid)",
result.stats.zone_of_pain_count
);
}
if result.stats.zone_of_uselessness_count > 0 {
println!(
" \x1b[33mZone of Uselessness: {} module(s)\x1b[0m (unstable, abstract)",
result.stats.zone_of_uselessness_count
);
}
println!();
if !result.stats.risk_distribution.is_empty() {
println!("Risk Distribution:");
for (risk, count) in &result.stats.risk_distribution {
let risk_level = match risk.as_str() {
"low" => CouplingRisk::Low,
"medium" => CouplingRisk::Medium,
"high" => CouplingRisk::High,
"critical" => CouplingRisk::Critical,
_ => CouplingRisk::Low,
};
println!(" {}{}: {}\x1b[0m", risk_level.color_code(), risk, count);
}
println!();
}
if !result.stats.most_depended.is_empty() {
println!("Most Depended Upon (highest Ca):");
for module in &result.stats.most_depended {
println!(" - {}", module);
}
println!();
}
if !result.stats.most_dependent.is_empty() {
println!("Most Dependent (highest Ce):");
for module in &result.stats.most_dependent {
println!(" - {}", module);
}
println!();
}
if show_cycles && !result.circular_dependencies.is_empty() {
println!("\x1b[31mCircular Dependencies Detected:\x1b[0m");
for (i, cycle) in result.circular_dependencies.iter().enumerate() {
println!(" {}. {} -> {}", i + 1, cycle.join(" -> "), cycle.first().unwrap_or(&String::new()));
}
println!();
}
println!("Modules (sorted by distance from main sequence):");
println!("{:-<100}", "");
println!(
"{:<40} {:>4} {:>4} {:>6} {:>6} {:>6} {}",
"Module", "Ca", "Ce", "I", "A", "D", "Position"
);
println!("{:-<100}", "");
for module in &result.modules {
let risk = CouplingRisk::from_distance(module.distance);
let position = module.position_description();
println!(
"{}{:<40} {:>4} {:>4} {:>6.2} {:>6.2} {:>6.2} {}\x1b[0m",
risk.color_code(),
if module.module.len() > 40 {
format!("{}...", &module.module[..37])
} else {
module.module.clone()
},
module.afferent,
module.efferent,
module.instability,
module.abstractness,
module.distance,
position
);
}
if show_edges && !result.edges.is_empty() {
println!();
println!("Dependency Edges:");
println!("{:-<80}", "");
for edge in &result.edges {
println!(
" {} -> {} [{}]{}",
edge.from,
edge.to,
edge.dependency_type,
if edge.items.is_empty() {
String::new()
} else {
format!(" ({})", edge.items.join(", "))
}
);
}
}
}
OutputFormat::Mermaid => {
println!("flowchart TD");
println!(" %% Coupling Analysis - {}", result.path.display());
for module in &result.modules {
let risk = CouplingRisk::from_distance(module.distance);
let style = match risk {
CouplingRisk::Low => "fill:#90EE90", CouplingRisk::Medium => "fill:#FFE4B5", CouplingRisk::High => "fill:#FFA07A", CouplingRisk::Critical => "fill:#FF6B6B", };
let node_id = module.module.replace('.', "_").replace('/', "_").replace(':', "_");
println!(
" {}[\"{}<br/>Ca={} Ce={} I={:.2}\"]",
node_id,
module.module,
module.afferent,
module.efferent,
module.instability
);
println!(" style {} {}", node_id, style);
}
for edge in &result.edges {
let from_id = edge.from.replace('.', "_").replace('/', "_").replace(':', "_");
let to_id = edge.to.replace('.', "_").replace('/', "_").replace(':', "_");
println!(" {} --> {}", from_id, to_id);
}
}
OutputFormat::Dot => {
println!("digraph coupling {{");
println!(" rankdir=LR;");
println!(" node [shape=box];");
println!(" // Coupling Analysis - {}", result.path.display());
println!();
for module in &result.modules {
let risk = CouplingRisk::from_distance(module.distance);
let color = match risk {
CouplingRisk::Low => "lightgreen",
CouplingRisk::Medium => "lightyellow",
CouplingRisk::High => "lightsalmon",
CouplingRisk::Critical => "lightcoral",
};
let node_id = module.module.replace('.', "_").replace('/', "_").replace(':', "_");
println!(
" {} [label=\"{}\\nCa={} Ce={}\\nI={:.2} D={:.2}\" fillcolor={} style=filled];",
node_id,
module.module,
module.afferent,
module.efferent,
module.instability,
module.distance,
color
);
}
println!();
for edge in &result.edges {
let from_id = edge.from.replace('.', "_").replace('/', "_").replace(':', "_");
let to_id = edge.to.replace('.', "_").replace('/', "_").replace(':', "_");
let style = match edge.dependency_type {
metrics::DependencyType::Import => "solid",
metrics::DependencyType::Call => "dashed",
metrics::DependencyType::Type => "dotted",
metrics::DependencyType::Inheritance => "bold",
};
println!(" {} -> {} [style={}];", from_id, to_id, style);
}
println!("}}");
}
OutputFormat::Csv => {
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
}
if result.stats.zone_of_pain_count > 0 || result.stats.zone_of_uselessness_count > 0 {
eprintln!();
if result.stats.zone_of_pain_count > 0 {
eprintln!(
"WARNING: {} module(s) in Zone of Pain (stable but concrete - hard to modify)",
result.stats.zone_of_pain_count
);
}
if result.stats.zone_of_uselessness_count > 0 {
eprintln!(
"WARNING: {} module(s) in Zone of Uselessness (abstract but unstable)",
result.stats.zone_of_uselessness_count
);
}
}
if !result.circular_dependencies.is_empty() {
eprintln!();
eprintln!(
"WARNING: {} circular dependency cycle(s) detected",
result.circular_dependencies.len()
);
}
Ok(())
}
fn cmd_cohesion(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
threshold: Option<u32>,
sort: bool,
violations_only: bool,
show_components: bool,
) -> Result<()> {
use metrics::{analyze_cohesion, CohesionLevel};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let mut result = analyze_cohesion(path, lang_str.as_deref(), threshold)
.map_err(|e| anyhow::anyhow!("Cohesion analysis failed: {}", e))?;
if sort {
result.classes.sort_by(|a, b| b.lcom4.cmp(&a.lcom4));
if let Some(ref mut violations) = result.violations {
violations.sort_by(|a, b| b.lcom4.cmp(&a.lcom4));
}
}
let output = if violations_only && result.violations.is_some() {
serde_json::json!({
"path": result.path,
"language": result.language,
"violations": result.violations,
"stats": result.stats,
"threshold": result.threshold,
})
} else {
if !show_components {
for class in &mut result.classes {
class.components.clear();
}
if let Some(ref mut violations) = result.violations {
for class in violations {
class.components.clear();
}
}
}
serde_json::to_value(&result).context("Failed to serialize result")?
};
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("Cohesion Analysis (LCOM)");
println!("========================");
println!("Path: {}", result.path.display());
if let Some(ref lang) = result.language {
println!("Language: {}", lang);
}
println!();
println!("Statistics:");
println!(" Total classes: {}", result.stats.total_classes);
println!(" Cohesive (LCOM3=1): {}", result.stats.cohesive_classes);
println!(" Low cohesion (LCOM3>1): {}", result.stats.low_cohesion_classes);
println!(" Average LCOM3: {:.2}", result.stats.average_lcom3);
println!(" Max LCOM3: {}", result.stats.max_lcom3);
println!(" Average methods: {:.1}", result.stats.average_methods);
println!(" Average attributes: {:.1}", result.stats.average_attributes);
println!();
if !result.stats.cohesion_distribution.is_empty() {
println!("Cohesion Distribution:");
for (level, count) in &result.stats.cohesion_distribution {
println!(" {}: {}", level, count);
}
println!();
}
let classes_to_show = if violations_only {
result.violations.as_ref().map(|v| v.as_slice()).unwrap_or(&[])
} else {
&result.classes
};
if classes_to_show.is_empty() {
if violations_only {
println!("No classes with low cohesion found.");
} else {
println!("No classes found.");
}
} else {
println!("Classes ({}):", classes_to_show.len());
for class in classes_to_show {
let level = CohesionLevel::from_lcom3(class.lcom3);
println!(
" {}{}: LCOM1={} LCOM3={} LCOM4={} methods={} attrs={} ({})\x1b[0m - {}:{}",
level.color_code(),
class.class_name,
class.lcom1,
class.lcom3,
class.lcom4,
class.methods,
class.attributes,
class.cohesion_level,
class.file.display(),
class.line
);
if class.is_low_cohesion {
if let Some(ref suggestion) = class.suggestion {
println!(" Suggestion: {}", suggestion);
}
}
if show_components && !class.components.is_empty() {
println!(" Connected components:");
for (i, component) in class.components.iter().enumerate() {
println!(" {}: {:?}", i + 1, component);
}
}
}
}
}
OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
}
if result.stats.low_cohesion_classes > 0 {
eprintln!();
eprintln!(
"WARNING: {} class(es) have low cohesion (LCOM3 > 1) - consider splitting",
result.stats.low_cohesion_classes
);
}
Ok(())
}
fn cmd_metrics_report(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
threshold_preset: &str,
sort_by: Option<String>,
issues_only: bool,
skip_coupling: bool,
fail_on: Option<&str>,
max_files: usize,
top: usize,
show_tokens: bool,
) -> Result<i32> {
use metrics::{
analyze_all_metrics, MetricsConfig, MetricThresholds, QualityGate,
FunctionSortBy, IssueSeverity, sort_unified_functions,
};
require_exists(path)?;
let lang_str = lang.map(|l| l.to_string());
let thresholds = match threshold_preset.to_lowercase().as_str() {
"strict" => MetricThresholds::strict(),
"relaxed" => MetricThresholds::relaxed(),
_ => MetricThresholds::default(),
};
let config = MetricsConfig {
thresholds,
include_halstead_tokens: show_tokens,
include_cognitive_breakdown: false,
include_cohesion_components: false,
analyze_coupling: !skip_coupling,
coupling_level: metrics::CouplingLevel::File,
max_files,
show_progress: false,
};
eprintln!("Analyzing metrics for: {}", path.display());
let start = std::time::Instant::now();
let mut report = analyze_all_metrics(path, lang_str.as_deref(), &config)
.map_err(|e| anyhow::anyhow!("Metrics analysis failed: {}", e))?;
if let Some(ref sort_field) = sort_by {
if let Ok(sort_by) = sort_field.parse::<FunctionSortBy>() {
sort_unified_functions(&mut report.function_metrics, sort_by);
}
}
let functions_to_show = if issues_only {
report.function_metrics.iter()
.filter(|f| {
!f.size_issues.is_empty() ||
f.cyclomatic_risk == metrics::RiskLevel::High ||
f.cyclomatic_risk == metrics::RiskLevel::Critical ||
f.cognitive_risk == metrics::CognitiveRiskLevel::High ||
f.cognitive_risk == metrics::CognitiveRiskLevel::Critical ||
f.maintainability_risk == metrics::MaintainabilityRiskLevel::High ||
f.maintainability_risk == metrics::MaintainabilityRiskLevel::Critical
})
.take(top)
.cloned()
.collect::<Vec<_>>()
} else {
report.function_metrics.iter().take(top).cloned().collect()
};
let elapsed = start.elapsed();
eprintln!("Analysis completed in {:.2}s", elapsed.as_secs_f64());
match format {
OutputFormat::Json => {
let output = if issues_only {
serde_json::json!({
"path": report.path,
"language": report.language,
"analysis_duration_ms": report.analysis_duration_ms,
"project_summary": report.project_summary,
"issues": report.issues,
"issue_stats": report.issue_stats,
"functions": functions_to_show,
})
} else {
serde_json::to_value(&report).context("Failed to serialize report")?
};
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("================================================================================");
println!(" UNIFIED METRICS REPORT");
println!("================================================================================");
println!();
println!("Path: {}", report.path.display());
if let Some(ref lang) = report.language {
println!("Language: {}", lang);
}
println!("Analysis duration: {}ms", report.analysis_duration_ms);
println!("Threshold preset: {}", threshold_preset);
println!();
println!("PROJECT SUMMARY");
println!("---------------");
println!(" Files analyzed: {}", report.project_summary.total_files);
println!(" Functions: {}", report.project_summary.total_functions);
println!(" Classes: {}", report.project_summary.total_classes);
println!();
println!(" Lines of Code:");
println!(" Physical: {}", report.project_summary.total_loc.physical);
println!(" Source (SLOC): {}", report.project_summary.total_loc.source);
println!(" Logical: {}", report.project_summary.total_loc.logical);
println!(" Comment: {}", report.project_summary.total_loc.comment);
println!();
println!(" Averages:");
println!(" Cyclomatic complexity: {:.2}", report.project_summary.avg_cyclomatic);
println!(" Cognitive complexity: {:.2}", report.project_summary.avg_cognitive);
println!(" Maintainability Index: {:.2}", report.project_summary.avg_maintainability);
println!(" Nesting depth: {:.2}", report.project_summary.avg_nesting);
println!(" Function size (SLOC): {:.2}", report.project_summary.avg_function_size);
println!();
println!(" Halstead Estimates:");
println!(" Estimated bugs: {:.2}", report.project_summary.total_estimated_bugs);
println!(" Estimated hours: {:.1}", report.project_summary.total_estimated_hours);
println!();
println!(" Quality Indicators:");
println!(" Files with critical issues: {}", report.project_summary.files_with_critical_issues);
println!(" Complex functions: {}", report.project_summary.complex_functions);
println!(" Low cohesion classes: {}", report.project_summary.low_cohesion_classes);
println!();
println!("ISSUE SUMMARY");
println!("-------------");
println!(" Total issues: {}", report.issue_stats.total);
println!(" \x1b[36mInfo: {}\x1b[0m", report.issue_stats.info);
println!(" \x1b[33mWarnings: {}\x1b[0m", report.issue_stats.warnings);
println!(" \x1b[31mCritical: {}\x1b[0m", report.issue_stats.critical);
println!();
if !report.issue_stats.by_category.is_empty() {
println!(" By category:");
let mut categories: Vec<_> = report.issue_stats.by_category.iter().collect();
categories.sort_by(|a, b| b.1.cmp(a.1));
for (category, count) in categories {
println!(" {}: {}", category, count);
}
println!();
}
if !report.issues.is_empty() {
let critical_issues: Vec<_> = report.issues.iter()
.filter(|i| i.severity == IssueSeverity::Critical)
.take(10)
.collect();
if !critical_issues.is_empty() {
println!("TOP CRITICAL ISSUES");
println!("-------------------");
for issue in critical_issues {
println!(
" \x1b[31m[CRITICAL]\x1b[0m {} - {}:{}",
issue.message,
issue.file.display(),
issue.line.map(|l| l.to_string()).unwrap_or_default()
);
if let Some(ref suggestion) = issue.suggestion {
println!(" Suggestion: {}", suggestion);
}
}
println!();
}
}
if !functions_to_show.is_empty() {
let header = if issues_only {
"FUNCTIONS WITH ISSUES"
} else {
"FUNCTION METRICS"
};
println!("{} (showing {})", header, functions_to_show.len());
println!("{}", "-".repeat(header.len() + 15));
for func in &functions_to_show {
let color = match func.cyclomatic_risk {
metrics::RiskLevel::Critical => "\x1b[35m",
metrics::RiskLevel::High => "\x1b[31m",
metrics::RiskLevel::Medium => "\x1b[33m",
metrics::RiskLevel::Low => "\x1b[32m",
};
println!(
" {}{}\x1b[0m - {}:{}",
color,
func.name,
func.file.display(),
func.line
);
println!(
" CC={} Cog={} MI={:.1} LOC={} Nest={} Params={}",
func.cyclomatic,
func.cognitive,
func.maintainability,
func.loc,
func.nesting,
func.params
);
if !func.size_issues.is_empty() {
for issue in &func.size_issues {
println!(" Issue: {}", issue);
}
}
}
println!();
}
if !report.class_metrics.is_empty() {
let low_cohesion: Vec<_> = report.class_metrics.iter()
.filter(|c| c.is_low_cohesion)
.take(10)
.collect();
if !low_cohesion.is_empty() {
println!("CLASSES WITH LOW COHESION");
println!("-------------------------");
for class in low_cohesion {
println!(
" {}: LCOM3={} LCOM4={} methods={} - {}:{}",
class.name,
class.lcom3,
class.lcom4,
class.method_count,
class.file.display(),
class.line
);
if let Some(ref suggestion) = class.suggestion {
println!(" {}", suggestion);
}
}
println!();
}
}
println!("================================================================================");
}
OutputFormat::Csv => {
if issues_only && !report.issues.is_empty() {
print!("{}", metrics::format_issues_csv(&report.issues));
} else {
print!("{}", metrics::format_functions_csv(&functions_to_show));
}
}
OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
let output = serde_json::to_value(&report).context("Failed to serialize report")?;
println!(
"{}",
serde_json::to_string_pretty(&output).context("Failed to serialize output")?
);
}
}
let exit_code = if let Some(fail_level) = fail_on {
let gate = match fail_level.to_lowercase().as_str() {
"warning" | "warnings" => QualityGate {
fail_on_critical: true,
max_critical_issues: 0,
max_warning_issues: 0,
min_maintainability: None,
max_avg_cyclomatic: None,
},
"critical" => QualityGate::default(),
_ => {
eprintln!("Unknown fail-on level: {}. Use 'warning' or 'critical'", fail_level);
QualityGate::default()
}
};
let result = gate.check(&report);
if result.failed {
eprintln!();
eprintln!("QUALITY GATE FAILED");
eprintln!("-------------------");
for reason in &result.reasons {
eprintln!(" - {}", reason);
}
1
} else {
eprintln!();
eprintln!("Quality gate passed.");
0
}
} else {
0
};
Ok(exit_code)
}
mod quality;
mod patterns;
fn cmd_quality(subcmd: QualityCommands) -> Result<()> {
match subcmd {
QualityCommands::Clones {
path,
min_lines,
lang,
format,
include_licenses,
include_tests,
include_generated,
max_file_size,
} => {
cmd_clones(
&path,
min_lines,
lang,
format,
include_licenses,
include_tests,
include_generated,
max_file_size,
)?;
}
QualityCommands::StructuralClones {
path,
similarity,
min_nodes,
min_lines,
lang,
format,
type2_only,
type3_only,
include_tests,
include_accessors,
include_interface_impls,
show_filtered,
max_file_size,
max_comparisons,
} => {
cmd_structural_clones(
&path,
similarity,
min_nodes,
min_lines,
lang,
format,
type2_only,
type3_only,
include_tests,
include_accessors,
include_interface_impls,
show_filtered,
max_file_size,
max_comparisons,
)?;
}
QualityCommands::GodClass {
path,
lang,
format,
threshold,
method_threshold,
attribute_threshold,
line_threshold,
lcom_threshold,
include_tests,
exclude_framework,
include_generated,
max_file_size,
min_severity,
} => {
cmd_god_class(
&path,
lang,
format,
threshold,
method_threshold,
attribute_threshold,
line_threshold,
lcom_threshold,
include_tests,
exclude_framework,
include_generated,
max_file_size,
&min_severity,
)?;
}
QualityCommands::LongMethod {
path,
lang,
format,
max_lines,
max_statements,
max_variables,
max_complexity,
max_nesting,
max_parameters,
strict,
lenient,
no_context,
show_suggestions,
min_severity,
sort,
} => {
cmd_long_method(
&path,
lang,
format,
max_lines,
max_statements,
max_variables,
max_complexity,
max_nesting,
max_parameters,
strict,
lenient,
no_context,
show_suggestions,
&min_severity,
sort,
)?;
}
QualityCommands::Circular {
path,
level,
lang,
format,
min_severity,
include_tests,
exclude_intentional,
max_cycles,
max_suggestions,
} => {
cmd_circular(
&path,
level,
lang,
format,
&min_severity,
include_tests,
exclude_intentional,
max_cycles,
max_suggestions,
)?;
}
QualityCommands::Patterns {
path,
pattern,
lang,
format,
min_confidence,
include_tests,
include_generated,
no_modern,
max_file_size,
} => {
cmd_patterns(
&path,
pattern.as_deref(),
lang,
format,
min_confidence,
include_tests,
include_generated,
no_modern,
max_file_size,
)?;
}
}
Ok(())
}
fn cmd_clones(
path: &PathBuf,
min_lines: usize,
lang: Option<Language>,
format: OutputFormat,
include_licenses: bool,
include_tests: bool,
include_generated: bool,
max_file_size: u64,
) -> Result<()> {
use quality::clones::{detect_clones, format_clone_summary, CloneConfig, TextualCloneDetector};
require_exists(path)?;
let mut config = CloneConfig::default();
config.min_lines = min_lines;
config.max_file_size = max_file_size;
config.exclude_license_headers = !include_licenses;
config.exclude_test_fixtures = !include_tests;
config.exclude_generated = !include_generated;
if let Some(l) = lang {
config.language = Some(l.to_string());
}
let detector = TextualCloneDetector::new(config);
let result = detector.detect(path)
.map_err(|e| anyhow::anyhow!("Clone detection failed: {}", e))?;
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("{}", format_clone_summary(&result));
}
OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
}
if result.stats.clone_groups > 0 {
eprintln!();
eprintln!(
"Detected {} clone group(s) with {} duplicated lines ({:.1}% duplication)",
result.stats.clone_groups,
result.stats.duplicated_lines,
result.stats.duplication_percentage
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn cmd_structural_clones(
path: &PathBuf,
similarity: f64,
min_nodes: usize,
min_lines: usize,
lang: Option<Language>,
format: OutputFormat,
type2_only: bool,
type3_only: bool,
include_tests: bool,
include_accessors: bool,
include_interface_impls: bool,
show_filtered: bool,
max_file_size: u64,
max_comparisons: usize,
) -> Result<()> {
use quality::clones::{
format_structural_clone_summary, StructuralCloneConfig, StructuralCloneDetector,
};
require_exists(path)?;
let mut config = StructuralCloneConfig::default();
config.similarity_threshold = similarity.clamp(0.0, 1.0);
config.min_nodes = min_nodes;
config.min_lines = min_lines;
config.max_file_size = max_file_size;
config.max_comparisons = max_comparisons;
config.filter_tests = !include_tests;
config.filter_accessors = !include_accessors;
config.filter_interface_impls = !include_interface_impls;
if type2_only && type3_only {
config.detect_type2 = true;
config.detect_type3 = true;
} else if type2_only {
config.detect_type2 = true;
config.detect_type3 = false;
} else if type3_only {
config.detect_type2 = false;
config.detect_type3 = true;
}
if let Some(l) = lang {
config.language = Some(l.to_string());
}
let detector = StructuralCloneDetector::new(config);
let mut result = detector
.detect(path)
.map_err(|e| anyhow::anyhow!("Structural clone detection failed: {}", e))?;
if !show_filtered {
result.clone_groups.retain(|c| !c.filtered);
}
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("{}", format_structural_clone_summary(&result));
}
OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
}
let total_groups = result.stats.type2_groups + result.stats.type3_groups;
if total_groups > 0 {
eprintln!();
eprintln!(
"Detected {} structural clone group(s): {} Type-2, {} Type-3",
total_groups, result.stats.type2_groups, result.stats.type3_groups
);
eprintln!(
" {} duplicated AST nodes across {} instances",
result.stats.duplicated_nodes, result.stats.clone_instances
);
if result.stats.filtered_groups > 0 && !show_filtered {
eprintln!(
" ({} potential false positive groups filtered, use --show-filtered to see them)",
result.stats.filtered_groups
);
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn cmd_god_class(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
threshold: f64,
method_threshold: u32,
attribute_threshold: u32,
line_threshold: u32,
lcom_threshold: u32,
include_tests: bool,
exclude_framework: bool,
include_generated: bool,
max_file_size: u64,
min_severity: &str,
) -> Result<()> {
use quality::smells::god_class::{
detect_with_config, format_god_class_summary, GodClassConfig, GodClassSeverity,
};
require_exists(path)?;
let mut config = GodClassConfig::default();
config.score_threshold = threshold;
config.method_threshold = method_threshold;
config.attribute_threshold = attribute_threshold;
config.line_threshold = line_threshold;
config.lcom_threshold = lcom_threshold;
config.exclude_tests = !include_tests;
config.exclude_framework = exclude_framework;
config.exclude_generated = !include_generated;
config.max_file_size = max_file_size;
if let Some(l) = lang {
config.language = Some(l.to_string());
}
let min_sev = match min_severity.to_lowercase().as_str() {
"low" => GodClassSeverity::Low,
"medium" => GodClassSeverity::Medium,
"high" => GodClassSeverity::High,
"critical" => GodClassSeverity::Critical,
_ => GodClassSeverity::Low,
};
let mut result = detect_with_config(path, config)
.map_err(|e| anyhow::anyhow!("God class detection failed: {}", e))?;
result.findings.retain(|f| f.severity >= min_sev);
let filtered_count = result.findings.len();
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("{}", format_god_class_summary(&result));
}
OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
}
if filtered_count > 0 {
eprintln!();
eprintln!(
"Detected {} God class(es) with severity >= {}",
filtered_count, min_severity
);
let mut critical = 0;
let mut high = 0;
let mut medium = 0;
let mut low = 0;
for finding in &result.findings {
match finding.severity {
GodClassSeverity::Critical => critical += 1,
GodClassSeverity::High => high += 1,
GodClassSeverity::Medium => medium += 1,
GodClassSeverity::Low => low += 1,
}
}
if critical > 0 {
eprintln!(" \x1b[35mCritical: {}\x1b[0m", critical);
}
if high > 0 {
eprintln!(" \x1b[31mHigh: {}\x1b[0m", high);
}
if medium > 0 {
eprintln!(" \x1b[38;5;208mMedium: {}\x1b[0m", medium);
}
if low > 0 {
eprintln!(" \x1b[33mLow: {}\x1b[0m", low);
}
eprintln!(
"\nAnalyzed {} classes, {} excluded (tests/framework/generated)",
result.stats.total_classes, result.stats.excluded_classes
);
} else {
eprintln!();
eprintln!("No God classes detected with severity >= {}", min_severity);
eprintln!(
"Analyzed {} classes, {} excluded",
result.stats.total_classes, result.stats.excluded_classes
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn cmd_long_method(
path: &PathBuf,
lang: Option<Language>,
format: OutputFormat,
max_lines: u32,
max_statements: u32,
max_variables: u32,
max_complexity: u32,
max_nesting: u32,
max_parameters: u32,
strict: bool,
lenient: bool,
no_context: bool,
show_suggestions: bool,
min_severity: &str,
sort: bool,
) -> Result<()> {
use quality::smells::long_method::{
detect_long_methods, format_long_method_summary, LongMethodConfig, LongMethodSeverity,
};
require_exists(path)?;
let mut config = if strict {
LongMethodConfig::strict()
} else if lenient {
LongMethodConfig::lenient()
} else {
LongMethodConfig {
max_lines,
max_statements,
max_variables,
max_complexity,
max_nesting,
max_parameters,
min_lines_for_analysis: 5,
context_aware: !no_context,
}
};
if no_context {
config.context_aware = false;
}
let language = lang.map(|l| l.to_string());
let min_sev = match min_severity.to_lowercase().as_str() {
"minor" => LongMethodSeverity::Minor,
"moderate" => LongMethodSeverity::Moderate,
"major" => LongMethodSeverity::Major,
"critical" => LongMethodSeverity::Critical,
_ => LongMethodSeverity::Minor,
};
let mut result = detect_long_methods(path, language.as_deref(), Some(config))
.map_err(|e| anyhow::anyhow!("Long method detection failed: {}", e))?;
result.findings.retain(|f| f.severity >= min_sev);
if sort {
result.findings.sort_by(|a, b| {
b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal)
});
}
let filtered_count = result.findings.len();
match format {
OutputFormat::Json => {
if !show_suggestions {
for finding in &mut result.findings {
finding.suggestions.clear();
finding.extraction_candidates.clear();
}
}
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
if show_suggestions {
println!("{}", format_long_method_summary(&result));
} else {
println!("=== Long Method Analysis ===\n");
println!("Path: {}", result.path.display());
if let Some(ref lang) = result.language {
println!("Language: {}", lang);
}
println!();
println!("Statistics:");
println!(" Total methods: {}", result.stats.total_methods);
println!(
" Long methods: {} ({:.1}%)",
result.stats.long_methods, result.stats.percentage_long
);
println!(" Average lines: {:.1}", result.stats.average_lines);
println!(" Max lines: {}", result.stats.max_lines);
println!();
if result.findings.is_empty() {
println!("No long methods found.");
} else {
println!("Long Methods ({}):\n", result.findings.len());
for finding in &result.findings {
println!(
"{}{}\x1b[0m at {}:{}-{}",
finding.severity.color_code(),
finding.function_name,
finding.file.display(),
finding.line,
finding.end_line
);
println!(
" {} lines, {} statements, {} vars, complexity {}, nesting {}",
finding.length.lines,
finding.length.statements,
finding.length.variables,
finding.complexity.cyclomatic,
finding.complexity.max_nesting
);
println!(
" Category: {}, Severity: {}",
finding.category, finding.severity
);
if !finding.violations.is_empty() {
for v in &finding.violations {
println!(" - {}", v);
}
}
println!();
}
}
}
}
OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
}
if filtered_count > 0 {
eprintln!();
eprintln!(
"Detected {} long method(s) with severity >= {}",
filtered_count, min_severity
);
let mut critical = 0;
let mut major = 0;
let mut moderate = 0;
let mut minor = 0;
for finding in &result.findings {
match finding.severity {
LongMethodSeverity::Critical => critical += 1,
LongMethodSeverity::Major => major += 1,
LongMethodSeverity::Moderate => moderate += 1,
LongMethodSeverity::Minor => minor += 1,
}
}
if critical > 0 {
eprintln!(" \x1b[35mCritical: {}\x1b[0m", critical);
}
if major > 0 {
eprintln!(" \x1b[31mMajor: {}\x1b[0m", major);
}
if moderate > 0 {
eprintln!(" \x1b[91mModerate: {}\x1b[0m", moderate);
}
if minor > 0 {
eprintln!(" \x1b[33mMinor: {}\x1b[0m", minor);
}
} else {
eprintln!();
eprintln!("No long methods detected with severity >= {}", min_severity);
}
eprintln!(
"Analyzed {} methods total",
result.stats.total_methods
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn cmd_circular(
path: &PathBuf,
level: CircularLevelArg,
lang: Option<Language>,
format: OutputFormat,
min_severity: &str,
include_tests: bool,
exclude_intentional: bool,
max_cycles: usize,
max_suggestions: usize,
) -> Result<()> {
use quality::circular::{
detect_circular_dependencies, format_circular_report, CircularConfig,
Severity as CircularSeverity,
};
require_directory(path)?;
let min_sev = match min_severity.to_lowercase().as_str() {
"low" => CircularSeverity::Low,
"medium" => CircularSeverity::Medium,
"high" => CircularSeverity::High,
"critical" => CircularSeverity::Critical,
_ => CircularSeverity::Low,
};
let mut config = CircularConfig::default();
config.level = level.into();
config.min_severity = min_sev;
config.include_tests = include_tests;
config.exclude_intentional = exclude_intentional;
config.max_cycles = max_cycles;
config.max_suggestions = max_suggestions;
if let Some(l) = lang {
config.language = Some(l.to_string());
}
let result = detect_circular_dependencies(path.to_str().unwrap_or("."), Some(config))
.map_err(|e| anyhow::anyhow!("Circular dependency detection failed: {}", e))?;
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("{}", format_circular_report(&result));
}
OutputFormat::Mermaid => {
println!("graph LR");
for cycle in &result.cycles {
for (from, to) in &cycle.cycle_path {
let from_id = from.replace('.', "_").replace("::", "_");
let to_id = to.replace('.', "_").replace("::", "_");
println!(" {}[\"{}\"] --> {}[\"{}\"]", from_id, from, to_id, to);
}
}
}
OutputFormat::Dot => {
println!("digraph circular_deps {{");
println!(" rankdir=LR;");
println!(" node [shape=box];");
for cycle in &result.cycles {
for (from, to) in &cycle.cycle_path {
println!(" \"{}\" -> \"{}\";", from, to);
}
}
println!("}}");
}
OutputFormat::Csv => {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
}
if result.stats.cycle_count > 0 {
eprintln!();
eprintln!(
"Detected {} circular dependency cycle(s) at {} level",
result.stats.cycle_count, result.config.level
);
if result.stats.critical_count > 0 {
eprintln!(" \x1b[35mCritical: {}\x1b[0m", result.stats.critical_count);
}
if result.stats.high_count > 0 {
eprintln!(" \x1b[31mHigh: {}\x1b[0m", result.stats.high_count);
}
if result.stats.medium_count > 0 {
eprintln!(" \x1b[91mMedium: {}\x1b[0m", result.stats.medium_count);
}
if result.stats.low_count > 0 {
eprintln!(" \x1b[33mLow: {}\x1b[0m", result.stats.low_count);
}
eprintln!();
eprintln!(
" {} participants in cycles, {} files involved",
result.total_participants_in_cycles, result.total_files_in_cycles
);
if !result.suggestions.is_empty() {
eprintln!();
eprintln!(
" {} breaking suggestion(s) generated",
result.suggestions.len()
);
}
} else {
eprintln!();
eprintln!(
"No circular dependencies detected at {} level",
result.config.level
);
}
eprintln!(
"Analyzed {} nodes, {} edges in {}ms",
result.stats.total_nodes, result.stats.total_edges, result.stats.analysis_time_ms
);
Ok(())
}
fn cmd_patterns(
path: &PathBuf,
pattern_filter: Option<&str>,
lang: Option<Language>,
format: OutputFormat,
min_confidence: f64,
include_tests: bool,
include_generated: bool,
no_modern: bool,
max_file_size: u64,
) -> Result<()> {
use patterns::{detect_patterns, format_pattern_summary, PatternConfig};
require_exists(path)?;
let mut config = PatternConfig::default();
config.min_confidence = min_confidence.clamp(0.0, 1.0);
config.max_file_size = max_file_size;
config.include_tests = include_tests;
config.include_generated = include_generated;
config.detect_modern_patterns = !no_modern;
if let Some(l) = lang {
config.language = Some(l.to_string());
}
let result = detect_patterns(path, pattern_filter, Some(config))
.map_err(|e| anyhow::anyhow!("Pattern detection failed: {}", e))?;
match format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
OutputFormat::Text => {
println!("{}", format_pattern_summary(&result));
}
OutputFormat::Mermaid | OutputFormat::Dot | OutputFormat::Csv => {
println!(
"{}",
serde_json::to_string_pretty(&result).context("Failed to serialize output")?
);
}
}
if result.stats.patterns_detected > 0 {
eprintln!();
eprintln!(
"Detected {} design pattern(s) in {} files",
result.stats.patterns_detected, result.stats.files_with_patterns
);
if !result.stats.by_category.is_empty() {
eprintln!(" By category:");
for (category, count) in &result.stats.by_category {
eprintln!(" {}: {}", category, count);
}
}
eprintln!(
" Average confidence: {:.1}%",
result.stats.average_confidence * 100.0
);
} else {
eprintln!();
eprintln!("No design patterns detected with confidence >= {:.0}%", min_confidence * 100.0);
}
eprintln!(
"Scanned {} files ({} skipped)",
result.stats.files_scanned, result.stats.files_skipped
);
Ok(())
}