AST-level code quality analyzer for 35 languages with 182 lint rules. Uses tree-sitter to parse source code into ASTs and analyzes complexity, lint issues, and structural quality — without executing code.
No language servers, no compilers, no code execution: one static binary parses everything from TypeScript to Lean 4 and scores it on the same scale.
$ codopsy analyze ./src -v
Analyzing ./src ...
Found 2 source file(s).
✗ ./src/Proof.lean (complexity: 1, cognitive: 0, issues: 1 error)
score A (91/100) — complexity 35/35, issues 31/40, structure 25/25
✗ ./src/parser.ts (complexity: 6, cognitive: 11, issues: 1 error, 3 warnings)
score B (80/100) — complexity 35/35, issues 20/40, structure 25/25
=== Analysis Summary ===
Quality Score: B (84/100)
Files analyzed: 2
Total issues: 6
Error: 2
Warning: 3
Info: 1
Avg complexity: 2.7
Max complexity: 6 (parse in ./src/parser.ts)
Scoring thresholds: cyclomatic >10, cognitive >15What it found in those two files:
Proof.lean:1 error no-sorry `sorry` leaves this declaration unproved
Proof.lean:3 info no-partial-def `partial` skips the termination checker
parser.ts:1 warning no-any Unexpected `any` type
parser.ts:2 warning eqeqeq Expected '===' instead of '=='
parser.ts:3 warning no-console Unexpected console statement
parser.ts:10 error no-eval eval() is not allowed
The full report is JSON — per file, per function, per issue (abbreviated here):
| Language | Extensions | Lint Rules | Complexity |
|---|---|---|---|
| TypeScript/TSX | .ts .tsx |
23 rules | CC + Cognitive |
| JavaScript | .js .jsx .mjs .cjs |
23 rules | CC + Cognitive |
| Python | .py .pyi |
17 rules | CC + Cognitive |
| Rust | .rs |
14 rules | CC + Cognitive |
| Go | .go |
14 rules | CC + Cognitive |
| Java | .java |
12 rules | CC + Cognitive |
| C/C++ | .c .h .cpp .cc .cxx .hpp .hxx |
9 rules | CC + Cognitive |
| Lean 4 | .lean |
8 rules | CC + Cognitive |
| Bash | .sh .bash .zsh |
7 rules | CC + Cognitive |
| Kotlin | .kt .kts |
6 rules | CC + Cognitive |
| Swift | .swift |
6 rules | CC + Cognitive |
| Ruby | .rb |
5 rules | CC + Cognitive |
| PHP | .php |
5 rules | CC + Cognitive |
| Haskell | .hs |
5 rules | CC + Cognitive |
| Scala | .scala .sc |
5 rules | CC + Cognitive |
| Dart | .dart |
5 rules | CC + Cognitive |
| Crystal | .cr |
5 rules | CC + Cognitive |
| Clojure | .clj .cljs .cljc .edn |
4 rules | CC + Cognitive |
| Elixir | .ex .exs |
4 rules | CC + Cognitive |
| Lua | .lua |
4 rules | CC + Cognitive |
| Zig | .zig |
4 rules | CC + Cognitive |
| Groovy | .groovy .gvy |
4 rules | CC + Cognitive |
| Julia | .jl |
4 rules | CC + Cognitive |
| Gleam | .gleam |
3 rules | CC + Cognitive |
| Erlang | .erl .hrl |
3 rules | CC + Cognitive |
| Elm | .elm |
3 rules | CC + Cognitive |
| C# | .cs |
universal | CC + Cognitive |
| OCaml | .ml .mli |
universal | CC + Cognitive |
| Almide | .almd |
universal | CC + Cognitive |
| HTML | .html .htm |
threshold | structure |
| CSS | .css |
threshold | structure |
| JSON | .json |
threshold | structure |
| YAML | .yml .yaml |
threshold | structure |
universal = todo-comment + no-empty-function + syntax-error + threshold rules.
Homebrew (macOS and Linux):
brew install o6lvl4/tap/codopsyPrebuilt binary — no toolchain required:
# macOS (Apple Silicon)
curl -sSfL https://github.com/O6lvl4/codopsy/releases/latest/download/codopsy-aarch64-apple-darwin.tar.gz | tar xz
# macOS (Intel)
curl -sSfL https://github.com/O6lvl4/codopsy/releases/latest/download/codopsy-x86_64-apple-darwin.tar.gz | tar xz
# Linux (x86_64)
curl -sSfL https://github.com/O6lvl4/codopsy/releases/latest/download/codopsy-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo mv codopsy /usr/local/bin/From source (Rust 1.80+; builds 35 tree-sitter grammars, so expect a few minutes):
cargo install --git https://github.com/O6lvl4/codopsy.git# Analyze a project
codopsy analyze ./src
# Verbose output (per-file details)
codopsy analyze ./src -v
# Output to stdout as JSON
codopsy analyze ./src -o -
# Only analyze changed files (vs main branch)
codopsy analyze ./src --diff main
# Show complexity hotspots (requires git)
codopsy analyze ./src --hotspots
# Save baseline for regression tracking
codopsy analyze ./src --save-baseline
# Fail CI if quality degrades
codopsy analyze ./src --no-degradation --fail-on-warning
# Initialize config
codopsy initcodopsy is a single binary with no runtime dependencies, so CI is a download and
a command. Exit code 1 on --fail-on-error (or --fail-on-warning) fails the job.
name: Quality
on: [push, pull_request]
jobs:
codopsy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install codopsy
run: |
curl -sSfL https://github.com/O6lvl4/codopsy/releases/latest/download/codopsy-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo mv codopsy /usr/local/bin/
- run: codopsy analyze ./src --fail-on-errorTo review only what a pull request touches, fetch enough history for the merge
base and point --diff at the target branch:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: codopsy analyze . --diff origin/${{ github.base_ref }} --fail-on-warningTo let existing debt stand but block new debt, commit a baseline
(codopsy analyze ./src --save-baseline writes .codopsy-baseline.json) and
gate on regressions instead of on absolute counts:
- run: codopsy analyze ./src --no-degradationEach file is scored 0–100 from three components, summed and rounded:
| Component | Weight | What it measures |
|---|---|---|
| Complexity | 35 | Per-function penalty for exceeding the cyclomatic/cognitive complexity thresholds |
| Issues | 40 | Lint violations, grouped by rule, weighted by severity |
| Structure | 25 | max-lines / max-depth / max-params violations |
The project score is a sqrt(function_count + 1)-weighted average of file
scores (files with more functions carry more weight), minus a small penalty
for the total number of issues scattered across the project.
The Complexity component uses the same max-complexity /
max-cognitive-complexity thresholds — whether from .codopsyrc.json or the
--max-complexity/--max-cognitive-complexity flags — that decide whether a
warning is emitted for a function. There is no separate, hidden threshold:
if you configure max-complexity: 20, a function at complexity 15 costs
nothing in either the issue list or the score. Each function's excess over
the threshold is penalized per unit (capped per function, so one outlier
can't dominate a file's score), and a file's Complexity component floors at 0
once the sum of its functions' excess crosses the 35-point budget — further
increases in complexity beyond that point don't cost additional points, but
they also don't recover any until the file's total excess drops back under
the budget. Disabling a rule ("max-complexity": false) removes it from the
score entirely, not just from the issue list.
Run with -v/--verbose to see the score breakdown per file, and the
scoringThresholds field in JSON output records exactly what was used for a
given run.
| Grade | Score |
|---|---|
| A | 90–100 |
| B | 75–89 |
| C | 60–74 |
| D | 40–59 |
| F | 0–39 |
Create .codopsyrc.json in your project root (or run codopsy init):
{
"rules": {
"no-console": "warning",
"no-debugger": "error",
"no-eval": "error",
"max-lines": { "severity": "warning", "max": 300 },
"max-depth": { "severity": "warning", "max": 4 },
"max-params": { "severity": "warning", "max": 4 },
"max-complexity": { "severity": "warning", "max": 10 },
"max-cognitive-complexity": { "severity": "warning", "max": 15 },
"no-println": false
},
"skipDirs": ["/custom-build/"],
"skipFiles": ["generated.ts"]
}Rules can be set to "warning", "error", "info", or false (disabled). Threshold rules accept { "severity": ..., "max": N }.
Config is searched upward from the target directory to the home directory.
| Rule | Default | Description |
|---|---|---|
no-any |
warning | Disallow any type |
no-console |
warning | Disallow console.* calls |
no-var |
warning | Disallow var declarations |
eqeqeq |
warning | Require ===/!== over ==/!= |
no-empty-function |
warning | Disallow empty function bodies |
no-nested-ternary |
warning | Disallow nested ternary expressions |
no-debugger |
error | Disallow debugger statements |
no-duplicate-case |
error | Disallow duplicate switch cases |
no-self-assign |
warning | Disallow self-assignment |
no-eval |
error | Disallow eval() |
no-unreachable |
error | Detect unreachable code after return/throw |
no-constant-condition |
warning | Disallow constant conditions (if (true)) |
default-case |
warning | Require default case in switch |
no-fallthrough |
warning | Disallow case fallthrough without break |
no-self-compare |
warning | Disallow x === x self-comparison |
no-useless-catch |
error | Disallow catch that only re-throws |
use-isnan |
error | Require Number.isNaN() instead of === NaN |
no-compare-neg-zero |
error | Disallow comparison with -0 |
no-unsafe-negation |
error | Disallow !key in obj (wrong precedence) |
no-constructor-return |
error | Disallow return with value in constructor |
valid-typeof |
error | Require valid typeof comparison strings |
no-useless-rename |
warning | Disallow import { x as x } |
no-empty-pattern |
warning | Disallow empty destructuring const {} = x |
| Rule | Default | Description |
|---|---|---|
no-unsafe |
warning | Disallow unsafe blocks |
no-unwrap |
warning | Disallow .unwrap() |
no-dbg |
warning | Disallow dbg!() macro |
no-todo |
warning | Disallow todo!()/unimplemented!() |
no-println |
info | Disallow println!()/print!() etc. |
no-empty-function |
warning | Disallow empty function bodies |
needless-bool |
warning | Simplify if c { true } else { false } |
needless-return |
warning | Remove explicit return in tail position |
bool-comparison |
warning | Simplify x == true to x |
collapsible-if |
warning | Merge if a { if b { } } to if a && b { } |
single-match |
warning | Prefer if let over single-arm match |
manual-map |
warning | Prefer .map() over match Some/None |
redundant-clone |
warning | Detect .clone().clone() |
eq-op |
warning | Detect self-comparison x == x |
| Rule | Default | Description |
|---|---|---|
no-panic |
warning | Avoid bare panic() |
no-fmt-print |
info | Avoid fmt.Println(), use structured logger |
no-ignored-error |
warning | Detect _ = err ignored errors |
no-os-exit |
warning | Avoid os.Exit() |
no-defer-in-loop |
warning | Avoid defer inside loops |
no-empty-block |
warning | Detect empty if/for blocks |
no-unreachable |
warning | Detect code after return/break |
no-naked-return |
warning | Avoid bare return with named returns |
no-range-over-string |
info | Flag range over string variable |
no-shadow-import |
warning | Detect variable shadowing import |
| Rule | Default | Description |
|---|---|---|
no-bare-except |
warning | Avoid bare except: |
no-print |
info | Avoid print(), use logging |
no-eval |
error | Disallow eval()/exec() |
no-mutable-default |
warning | Disallow mutable default arguments |
no-global |
warning | Avoid global keyword |
no-assert |
info | Avoid assert in non-test code |
unreachable |
warning | Detect code after return/raise |
pointless-except |
warning | Detect except that only re-raises |
no-pass-body |
info | Detect function/class with only pass |
no-star-import |
warning | Avoid from x import * |
no-nested-with |
warning | Combine nested with statements |
no-return-in-init |
error | Disallow return value in __init__ |
simplify-boolean-return |
warning | Simplify if c: return True else: return False |
| Rule | Default | Description |
|---|---|---|
no-sysout |
warning | Avoid System.out.println() |
no-print-stack-trace |
warning | Avoid e.printStackTrace() |
no-empty-catch |
warning | Disallow empty catch blocks |
no-throws-exception |
warning | Avoid throws Exception (too broad) |
no-raw-type |
warning | Require generics on collection types |
no-string-equality |
warning | Use .equals() instead of == for strings |
missing-switch-default |
warning | Require default in switch |
no-empty-if |
warning | Detect empty if blocks |
no-double-brace-init |
warning | Avoid new X() {{ }} initialization |
no-string-concat-in-loop |
warning | Use StringBuilder in loops |
no-nested-try |
warning | Avoid nested try blocks |
equals-null |
error | Detect x.equals(null) |
| Rule | Default | Description |
|---|---|---|
no-printf |
info | Avoid printf()/sprintf() in production |
no-unsafe-fn |
error | Disallow gets()/strcpy()/strcat() |
no-malloc |
info | Flag malloc()/calloc() usage |
no-goto |
warning | Avoid goto statements |
no-sizeof-ptr |
warning | Detect sizeof(ptr) on pointers |
no-magic-number |
info | Flag numeric literals (not 0/1) |
no-implicit-fallthrough |
warning | Require break in switch cases |
no-empty-if |
warning | Detect empty if blocks |
no-void-main |
warning | Use int main() not void main() |
| Rule | Default | Description |
|---|---|---|
no-sorry |
error | sorry leaves a declaration unproved |
no-axiom |
warning | axiom adds an unproved assumption |
no-native-decide |
warning | native_decide trusts the compiler, not just the kernel |
no-unsafe |
warning | unsafe bypasses soundness guarantees |
no-unlimited-heartbeats |
warning | set_option maxHeartbeats 0 removes the elaboration timeout |
no-partial-def |
info | partial skips the termination checker |
no-dbg-trace |
info | dbg_trace is leftover debug output |
no-debug-command |
info | #eval / #check / #print / #reduce left in the file |
Lean is macro-extensible, so no tree-sitter grammar can cover every file. codopsy
parses what it can and reports the rest via the universal syntax-error rule
instead of silently scoring an unread file as clean. Hand-written Lean parses
essentially in full; the deepest metaprogramming in lean4/Mathlib core does not.
| Rule | Default | Description |
|---|---|---|
no-io-inspect |
warning | Disallow IO.inspect() |
no-io-puts |
info | Disallow IO.puts() |
no-raise-in-with |
warning | Avoid raise inside with blocks |
pipe-into-anonymous |
warning | Avoid piping into anonymous functions |
| Rule | Default | Description |
|---|---|---|
no-println |
info | Disallow println/prn/print |
no-def-in-def |
warning | Disallow nested def/defn |
no-thread-sleep |
warning | Avoid Thread/sleep |
no-reflection |
warning | Flag Java reflection calls |
| Rule | Default | Description |
|---|---|---|
no-process-flag |
warning | Flag process_flag usage |
no-catch-all |
warning | Detect catch-all as first clause |
no-exit-call |
warning | Avoid exit() calls |
| Rule | Default | Description |
|---|---|---|
no-todo |
warning | Disallow todo expressions |
no-panic |
warning | Disallow panic expressions |
no-let-assert |
warning | Avoid let assert (crashes at runtime) |
| Rule | Default | Description |
|---|---|---|
no-println |
info | Avoid println() |
no-unsafe-cast |
warning | Avoid as cast; use as? |
no-not-null-assertion |
warning | Avoid !!; use safe calls |
no-empty-catch |
warning | Disallow empty catch blocks |
no-system-exit |
warning | Avoid System.exit() |
prefer-val |
info | Prefer val over var |
| Rule | Default | Description |
|---|---|---|
no-puts |
info | Avoid puts/p/pp |
no-eval |
error | Disallow eval |
require-relative |
warning | Use require_relative for relative paths |
no-rescue-exception |
warning | Avoid rescue Exception |
no-sleep |
warning | Avoid sleep in production |
| Rule | Default | Description |
|---|---|---|
no-debug-output |
warning | Remove var_dump/dd |
no-eval |
error | Disallow eval() |
no-exit |
warning | Avoid die()/exit() |
strict-comparison |
warning | Use === instead of == |
no-error-suppression |
warning | Avoid @ operator |
| Rule | Default | Description |
|---|---|---|
unquoted-expansion |
warning | Quote $var to prevent word splitting |
no-eval |
error | Disallow eval |
cd-without-or |
warning | Use cd dir || exit 1 |
useless-cat |
warning | Use cmd < file instead of cat file | cmd |
dangerous-rm |
error | Flag rm -rf with variable expansion |
no-set-e |
info | Add set -euo pipefail |
test-equals |
warning | Use = not == in [ ] |
| Rule | Default | Description |
|---|---|---|
no-print |
info | Avoid print(); use os_log |
no-force-unwrap |
warning | Avoid ! force unwrapping |
no-force-try |
warning | Avoid try! |
no-force-cast |
warning | Avoid as! force cast |
no-nslog |
warning | Avoid NSLog |
no-fatal-error |
warning | Avoid fatalError() |
Each has 3–5 dedicated rules targeting debug output, unsafe patterns, and language-specific anti-patterns. See the source in src/analyzer/rules/ for full details.
| Rule | Default | Description |
|---|---|---|
todo-comment |
info | Detect TODO/FIXME/HACK/XXX comments |
no-empty-function |
warning | Detect empty function bodies |
syntax-error |
info | Source the parser could not read (results for that file are incomplete) |
| Rule | Default | Description |
|---|---|---|
max-lines |
300 | Maximum lines per file |
max-depth |
4 | Maximum nesting depth |
max-params |
4 | Maximum function parameters |
max-complexity |
10 | Maximum cyclomatic complexity per function |
max-cognitive-complexity |
15 | Maximum cognitive complexity per function |
- Parse: tree-sitter converts source code into a language-agnostic AST
- Analyze: Walk the AST to compute cyclomatic/cognitive complexity and detect lint violations
- Score: Weighted scoring across complexity, issues, and structure
- Report: JSON output with per-file and per-function details
All analysis is static — no code execution required. Files are analyzed in parallel via rayon.
cargo test # unit tests + language E2E fixtures
codopsy analyze ./src # codopsy scores itself; CI requires this to stay cleanTwo suites shell out to each language's real toolchain — e2e_syntax checks that
the "clean" fixtures actually compile, and e2e_compare diffs codopsy's findings
against the native linters. Both skip themselves when a toolchain is missing, so
cargo test passes without them. To run them for real, install the versions
pinned in qusp.toml:
qusp install
cargo test --test e2e_syntax --test e2e_compareAdding a language means one row in LANGUAGES (src/analyzer/language.rs), a
rule table in src/analyzer/rule_tables.rs, and a fixture under
tests/fixtures/ whose expect: comment lists the rules it should trigger.
MIT

{ "score": { "overall": 84, "grade": "B", "distribution": { "A": 1, "B": 1, "C": 0, "D": 0, "F": 0 } }, "summary": { "totalFiles": 2, "totalIssues": 6, "averageComplexity": 2.6666666666666665 }, "scoringThresholds": { "cyclomaticComplexity": 10, "cognitiveComplexity": 15 }, "files": [ { "file": "./src/parser.ts", "complexity": { "cyclomatic": 6, "cognitive": 11, "functions": [ { "name": "parse", "line": 1, "complexity": 6, "cognitiveComplexity": 11 } ] }, "issues": [ { "line": 10, "column": 11, "severity": "error", "rule": "no-eval", "message": "eval() is not allowed" } ], "score": { "score": 80, "grade": "B" } } ] }