Skip to content

Your first run

Point it at a directory:

Terminal window
npx bonsai-lint --over 15 src/

Three columns come back, worst first:

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.

Run it with no arguments at all and it still reports, 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.

Terminal window
npx bonsai-lint src/ # same as --over 15

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 and minified output. 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 both.