Skip to content

Your first run

Point it at a directory:

Terminal window
npx bonsai-lint src/

Three columns come back, worst first:

Terminal window
69 packages/billing/src/invoice-mapper.service.ts:47 InvoiceMapperService::mapLineItems
25 services/api/src/Controller/CheckoutController.php:207 CheckoutController::applyDiscounts
22 packages/web/src/parser/lexer.js:19 Lexer

Score, then the file and the line the declaration starts on, then the qualified name of the unit. That is the whole format. A clean run prints nothing at all and exits 0.

The run above passed no threshold and still reported, because 15 is the default for every language. That is a widely used default for a metric of this kind, and it is what this documentation uses in examples.

Treat it as a starting point rather than an authority. A threshold is a claim about what your team will tolerate, and 15 is a claim somebody else made about code they have not read. --over changes it for one run; threshold in bonsai-lint.toml changes it for the project.

The first run is more useful as a question, what is actually in here?, than as a verdict. --all scores every unit and reports all of them, so you see the distribution rather than guess at it:

Terminal window
npx bonsai-lint --all src/ | head -30

If most of your code sits under 10 and a handful of functions are in the 40s, the number to argue about is obvious. Sort by the score and pick a threshold from what you find.

Cognitive complexity measures how hard code is to read, not how hard it is to test. A switch with twenty arms is cyclomatically awful and cognitively fine; three nested ifs are the reverse. Increments come from breaks in linear flow, and each one costs more the deeper it is nested, which is why a callback pyramid scores far above the sum of its parts.

The scoring rules page has the full increment table. The short version:

  • +1 for each branch, loop, catch, or run of boolean operators
  • +1 more for every level of nesting the construct sits inside
  • nesting compounds through closures, so a callback inside a loop inside a condition is scored at the depth it genuinely sits at

Every supported file under the paths you name, minus declaration files, minified output and files that mark themselves as generated. You do not select a language. Point it at a PHP project and it behaves as a PHP linter, point it at a monorepo and it scores everything in one pass on one metric.

See languages and extensions for the mapping, and cross-language parity for why one threshold is meaningful across all of them.