abcop 0.16.2

Must-have ABC complexity gate for AI development. Ruby, Rust, Python, Go, JS/TS, C/C++, PHP, Java, C#, Swift, Zig, Dart, Solidity, ObjC
.TH ABCOP 1 "2026-08-30" "abcop 0.16.2" "User Commands"
.SH NAME
abcop \- must\-have ABC complexity gate for AI development. Ruby, Rust, Python, Go, JS/TS, C/C++, PHP, Java, C#, Swift, Zig, Dart, Solidity, ObjC
.SH SYNOPSIS
.B abcop
.RI [ OPTIONS ]
.RI [ PATH ].\&.\&.
.PP
.B abcop \-\-dump\-tree
.IR FILE
.PP
Bare
.B abcop
scans for what you are working on right now: uncommitted changes against
HEAD when there are any, otherwise your current merge request (only
functions on lines changed since branching from the default branch, or
the last 36 hours when committing directly onto it). Outside a
repository \-\- or when no base can be detected \-\- it falls back to
walking the full current directory.
Use
.B \-\-full
for a deliberate whole\-tree scan,
.B \-\-uncommitted
for only your working\-tree edits, and
.B \-\-everything
to lift every filter.
.PP
Default walks prune test and fixture trees
.RB ( spec/, " tests/, " fixtures/, " testdata/),
vendored dependency and build\-output trees
.RB ( vendor/, " node_modules/, " target/, " dist/, " build/, " coverage/),
the
.I db/migrate
sequence, framework route tables
.RB ( config/routes.rb,
.IR config/routes/*.rb ),
and minified bundles
.RB ( *.min.js, " *.bundle.js ).
MR scopes also drop framework route tables, and a bare invocation
prefers uncommitted work against HEAD when the tree is dirty, covering
the branch's changes against its base otherwise.
A path named explicitly on the command line is walked in full regardless.
.SH DESCRIPTION
.B abcop
is a static\-analysis utility in the spirit of
.BR rubocop (1)
and
.BR lizard (1),
built for speed and for AI\-era maintainability.
It parses source with tree\-sitter grammars (no language runtime required)
and gates
.B complexity
via the ABC metric at function scope
.RI ( Metrics/AbcSize )
and module scope
.RI ( Metrics/ModuleAbcSize ),
so units stay small enough for humans and LLM agents to review.
Secondary checks flag one\-shot locals that are safe to inline and
bindings that are never read.
.PP
Two design rules distinguish it from general\-purpose linters:
.TP
a) it does one parse per file and one walk per metric, with file\-level data
parallelism, so it runs one to two orders of magnitude faster than full
linting pipelines on the same trees;
and
b) its AbcSize implementation matches RuboCop's default
\fIMetrics/AbcSize\fP
counting (verified against RuboCop 1.89 on real\-world corpora), so existing
thresholds and baselines carry over.
.SH METRICS
.SS Metrics/AbcSize
For every function\-like unit, abcop counts
.B assignments
(A: plain and compound assignments, multiple\-assignment targets, pattern and
rescue bindings, closure/loop parameters of nested callables),
.B branches
(B: calls, method calls, operators, yields), and
.B conditions
(C: comparisons, logical operators, loops, conditionals, match arms,
exception handlers). The score is sqrt(A^2 + B^2 + C^2), rounded to two
decimals. Units scoring strictly above
.I max
are reported with a vector
.IB < A , " B , " C >
in the message.
.IP \(bu
Ruby units are
.B def,
.B defs
and
.B define_method(:sym){}
blocks; ordinary iteration blocks roll into the enclosing method, mirroring
RuboCop.
.IP \(bu
Rust units are
.B fn
items (free functions and inherent/trait methods); closures roll into the
enclosing function. Match arms each count as one condition, guards via their
binary expressions, and the ? try operator counts as a branch.
.SS ModuleAbcSize
Production modules whose summed method ABC exceeds
.I max\-module\-abc
(default 120) are flagged for
extraction (Fitzpatrick total of method vectors). Test suites and test
directories, lockfiles, schema dumps and documentation trees are exempt.
For Rust files the trailing
.B #[cfg(test)]
module does not count toward the budget.
.SS UsedOnce
A local variable that is written exactly once by a plain assignment and read
exactly once afterwards is reported as an inline candidate when the rewrite is
provably safe:
.TP
\(bu the right\-hand side is pure (literals, constants, arithmetic/comparison
compositions); calls, macros, try operators and references to other locals
disqualify it;
.TP
\(bu the write executes unconditionally (not nested in if/match/loop/rescue);
.TP
\(bu the single read happens after the write and outside macro input tokens;
.TP
\(bu parameters, pattern bindings, rescue variables and re\-assigned or
shadowed names never qualify.
.SS NeverUsed
A local variable that is assigned but never read is dead code. abcop
reports it once per binding at the first write. Underscore-prefixed names,
parameters, enum constructors in patterns and reads inside macro input or
format-string captures all count as uses or exemptions as appropriate.
.SH SUPPORTED LANGUAGES
Language;Extensions / files;Checks
Ruby;.rb .rake .ru .gemspec plus Gemfile/Rakefile;all four, RuboCop\-parity counting
Rust;.rs;all four
Python;.py .pyi .pyw;all four
Go;.go;all four
PHP;.php;all four
Java;.java;all four
C#;.cs;all four
Solidity;.sol;all four
JavaScript;.js .mjs .cjs .jsx;all four
TypeScript;.ts .tsx .mts .cts;all four
C/C++;.c .h .cc .cpp .cxx .hpp .hxx .hh;all four (.h via C++ grammar)
Objective\-C;.m .mm;all four
Swift;.swift;all four
.br
Every language is scored on the same engine: named declarations are units,
anonymous function\-likes roll into the enclosing unit, assignments count
A, calls/new/message sends/yield/throw and arithmetic operators count B,
and if/ternary/guard, loops, switch arms, catch clauses, comparisons and
logical operators count C. UsedOnce, NeverUsed, purity\-gated inlining
and ModuleAbcSize run on every language through one scope\-model engine:
static spec tables describe each grammar (which kinds bind, read, open
scopes), and a single dispatcher evaluates everything the tables do not
cover.
.SH DIRECTIVES
In every supported language except Rust abcop honours RuboCop suppression
comments (`#` or `//`), so existing codebases do not need new markers:
.TP
.B # rubocop:disable Metrics/AbcSize
disables from this line until a matching enable or end of file.
.TP
.B # rubocop:disable Metrics/AbcSize ... (trailing code on the line)
suppresses the offense reported for this line only.
.TP
.B # rubocop:enable Metrics/AbcSize
closes an open range.
.TP
.B # rubocop:disable
with no cop list disables every abcop check in range.
.TP
.B # rubocop:todo ...
behaves like disable.
.TP
.B # rubocop:disable-next ...
is not a RuboCop directive and is ignored, matching rubocop itself.
.TP
.B Metrics
as a bare namespace name covers all Metrics/* checks including AbcSize.
.PP
Directives are text based and also apply to Rust sources, where
.B // rubocop:disable Metrics/AbcSize
on a definition line suppresses its report.
.SH OPTIONS
.TP
.BI \-\-format " FORMAT"
Output format:
.B text
(human readable, default),
.B json
(machine readable; one object with a
.I diagnostics
array of
.IR file ", " line ", " column ", " severity ", " rule ", " message
and, for ABC entries, numeric
.I score
and
.IR vector ,
then
.I files
and
.IR elapsed_ms .
Without
.B \-\-sort\-by\-score
the array streams as each file finishes),
or
.B jsonl
(JSON Lines: the same diagnostic objects one per line, no wrapping
document; streams as each file finishes unless
.B \-\-sort\-by\-score
buffers first).
.TP
.BI \-\-max\-abc " N"
Maximum ABC score before reporting (default 17, matching RuboCop).
.TP
.BI \-\-max\-module\-abc " N"
Maximum module ABC score before reporting (default 120).
.TP
.BI \-\-only " CHECK"
Run only one of
.B abc
or
.BR used-once
or
.BR never-used .
.TP
.B \-\-mr
Scan your current merge request explicitly: changes since branching from
master/main (merge\-base), or \-\- when committing directly onto the default
branch \-\-
.RB "last 36 hours"
.RB ( default_branch "@{" 36.hours.ago "}),
unioned with uncommitted working\-tree edits.
The default branch is detected among origin/main, origin/master, main and
master. The bare default picks uncommitted work alone when the working
tree is dirty; pass
.B \-\-mr
to force this union regardless.
.TP
.B \-\-uncommitted
Scan only uncommitted work: working\-tree and index edits against HEAD
plus untracked files. No branch/base diff is computed, so the scope is
exactly what would be staged or committed next \-\- what the bare default
already prefers when such work exists. Fails outside a repository
instead of widening to a full\-tree scan.
.TP
.B \-\-full
Scan the whole production tree instead of the scoped run; the default
pruning of vendored/generated/test material stays active. Bare
.B \-\-full
targets the current directory.
.TP
.B \-\-everything
Scan literally everything below the target: gitignore files are not
honoured, hidden directories are entered, and vendored/generated/test
pruning is disabled. The escape hatch for auditing what the defaults hide.
.TP
.B \-\-sort\-by\-score
Buffer every finding and emit highest\-score diagnostics first
(method and module ABC descending; UsedOnce / NeverUsed after scored
entries). Without this flag, text and JSON print each file as soon as
analysis finishes.
.TP
.B \-\-no\-cache
Skip the on\-disk result cache for this run, forcing a full reanalysis.
.TP
.B \-\-dump\-tree " FILE"
Debug aid: print the tree\-sitter syntax tree of a single file and exit.
.TP
.B \-\-help
Print help.
.TP
.B \-\-version
Print version.
.SH FILE ORDER
Files are analysed in parallel. Text and JSON print each file as soon as
its analysis finishes (completion order across files). JSON remains one
object:
.I diagnostics
opens first and grows, then
.I files
/
.I elapsed_ms
close the document. With
.B \-\-sort\-by\-score
results are buffered and emitted largest ABC first.
.SH CACHING
Results for unchanged files are cached in the user\-wide XDG cache dir
(
.IB $XDG_CACHE_HOME/abcop
or
.IR ~/.cache/abcop ;
override with
.BR $ABCOP_CACHE_DIR ).
Keys hash file contents plus tool version, rule revision, threshold,
selected checks and path, so stale entries cannot be served and entries
from different projects never collide. Entries live in a single embedded
database file,
.IR cache.redb .
The cache is auto\-pruned to the
20 000 newest entries.
.TP
.B \-\-no\-cache
Disable reading and writing the cache.
.SH EXIT STATUS
.TP
.B 0
No offenses or warnings were found.
.TP
.B 1
At least one diagnostic was reported.
.TP
.B 2
Usage error (bad arguments, unreadable input).
.SH EXAMPLES
Scan a Rails application with the default threshold and print human readable
results:
.PP
.RS
.nf
abcop app lib
.fi
.RE
.PP
Emit JSON for tooling, tolerating a custom threshold:
.PP
.RS
.nf
abcop \-\-format json \-\-max\-abc 12 lib > abcop.json
.fi
.RE
.PP
Emit JSON Lines for streaming consumers:
.PP
.RS
.nf
abcop \-\-format jsonl lib > abcop.jsonl
.fi
.RE
.PP
Only inline candidates, ignoring ABC size:
.PP
.RS
.nf
abcop \-\-only used\-once src
.fi
.RE
.PP
Inspect how a construct was parsed (any supported language):
.PP
.RS
.nf
abcop \-\-dump\-tree tricky.rs | less
.fi
.RE
.SH PERFORMANCE
On an Apple M1 Pro laptop, scanning the RuboCop source tree (943 files,
110k lines of Ruby) takes about 0.13s for both metrics, versus roughly 5s
for
.B "rubocop \-\-only Metrics/AbcSize"
with caching disabled; throughput on a 2.6M line Rust corpus is about
940k lines per second. Results are deterministic across runs.
.SH SEE ALSO
.BR rubocop (1),
.BR lizard (1)
.PP
ABC metric: A. J. Albrecht et al., c2.com/cgi/wiki?AbcMetric
.SH COPYRIGHT
Copyright \(co 2026 Peter Adrianov. All rights reserved.
.PP
Released under the GNU General Public License v3 or later
(GPL\-3.0\-or\-later); see the LICENSE file in the source distribution.

.SH AUTHORS
Peter Adrianov