npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@neralabs/rerail-pack-complexity

v0.1.4

Published

Rerail rule pack: cyclomatic complexity limits for Python, TypeScript and JavaScript functions.

Readme

Complexity opt-in rule pack

Cyclomatic complexity rules for Python, TypeScript and JavaScript, with the decision points of lizard 1.24.0 and without its tokenizer defects. A switch or match counts once however many cases it has, as slapkit scores a function with the lower of lizard's standard count and its modified count (-m), so a flat dispatch table is not a reason to split a function. The rule decides per switch, so a default-only switch beside a cased one counts 0 where slapkit's whole-function minimum gives it 1 (see "Deviations"). One document per language, rule id complexity-cyclomatic-<language>. They form the complexity rule pack, which set-up offers for a repository with Python, TypeScript or JavaScript source outside tests and build output; once a repository adds it, its rules are on (see docs/rule-packs.md). Every document has mode: "steer": a rule that runs steers the agent and never blocks.

| Rule | Files | |---|---| | complexity-cyclomatic-python | .py | | complexity-cyclomatic-typescript | .ts, .tsx, .mts, .cts | | complexity-cyclomatic-javascript | .js, .jsx, .mjs, .cjs |

A function's cyclomatic complexity is 1 plus its decision points. A function fails when that exceeds 6, so 7 or more. The finding spans the whole function, so under the ai-output scope only functions an edit touched are reported. The message names the function and its number, as slapkit's advisory does: `grade` has cyclomatic complexity 7, over the ceiling of 6. Decompose it into smaller functions. (code CYCLOMATIC_COMPLEXITY). A TypeScript or JavaScript function with no name of its own takes the variable, class field or object key it is assigned to; a callback with none of those is "This function" (the too-complex-anonymous clause). The number comes from the count's as/offset binding: the rule counts decision points with count.has (stopBy the function boundary, so a nested function is not entered), binds the count plus 1 as $CCN, and fires at min: 6 decisions.

Test files are out of scope: Python **/test_*.py, **/*_test.py, **/tests/**, **/conftest.py; TypeScript and JavaScript **/*.test.*, **/*.spec.*, **/__tests__/**. So are dependency, virtual-environment, build-output and generated directories (node_modules, .venv, dist, build, generated, ...).

What counts

Python, per def (async, decorated and methods included):

  • if, elif, for and async for, while, except, finally, each and and each or, and each match once, however many cases it has (the wildcard included);
  • the if of a conditional expression, and every for and if clause of a comprehension; a case guard is an if too;
  • not else, with, assert, try or return.

A nested def is scored on its own and adds nothing to its parent. A lambda is not a function here: its conditions count toward the enclosing def, as lizard counts them.

TypeScript and JavaScript, per function declaration, function expression, arrow function, method, getter, setter, constructor and generator function:

  • if (else if included), for, for...in, for...of, while, do...while, catch, each switch once however many cases it has (a switch with only default counts 0), &&, ||, ||=, &&= and the ?: conditional;
  • not optional parameters or properties (a?: T), optional chaining (?.), default parameters, ?? or ??=.

Each function is scored on its own: decisions inside a nested function do not count for the function around it. A class is not scored itself.

Tree-sitter node kinds, from the grammars' node-types.json (tree-sitter-python, tree-sitter-typescript and tree-sitter-javascript as vendored under native/grammars/):

| Language | Function boundary | Decision | |---|---|---| | Python | function_definition | if_statement, elif_clause, for_statement, while_statement, except_clause, finally_clause, boolean_operator (one per operator), conditional_expression, for_in_clause, if_clause, match_statement | | TypeScript, JavaScript | function_declaration, function_expression, arrow_function, method_definition, generator_function, generator_function_declaration | if_statement, for_statement, for_in_statement (in and of), while_statement, do_statement, catch_clause, switch_statement with a switch_case in its switch_body, ternary_expression, binary_expression with && or \|\|, augmented_assignment_expression with &&= or \|\|= |

Deviations from lizard 1.24.0

Each but the last is a lizard defect found on real code (see "Checked on real code"); the last is a choice in how a switch counts. Each has a corpus case.

  • ?? and ??= count 0. lizard's tokenizer reads each as two ? and counts 2.
  • A ? inside a regex literal counts 0, and so does ?. right after ) or ] (f()?.x, a[0]?.x). lizard reads each as a conditional.
  • An arrow function whose expression body starts on a later line is scored on its own. lizard adds its conditions to the enclosing function and lists a one-line arrow, scored 1, on the line after.
  • A Python def whose return annotation spans several lines, or whose parameter list breaks after a default holding brackets, is scored whole. lizard ends the def inside its signature and scores it 1 (fixed in crapkit #72). A parameter list that only spans several lines is read correctly by both.
  • ?. counts 0 in .jsx files too. lizard's JSX reader counts each ?. as a conditional; its .js and .ts readers do not, and neither does Rerail anywhere.
  • lizard does not read .mts or .cts files at all; the TypeScript rule covers them with the same grammar.
  • A default-only switch counts 0 beside a cased one. The recorded number is the lower of lizard's standard and -m counts over the whole function, and there -m's 1 for a switch with no case survives; the rule counts each switch on its own (defaultOnlyBesideCasedSwitch in the TypeScript and JavaScript corpus).

Checked on real code

The rules were compared with lizard 1.24.0 function by function on released sources (2026-10-07), pairing each lizard function with the Rerail function that starts within two lines of it:

| Repository | Rule | Paired | Same number | The rest | |---|---|--:|--:|---| | jinja 3.1.4 src | Python | 747 | 743 | 4 defs with a multi-line return annotation (crapkit #72); collapsing each signature onto one line makes lizard agree with Rerail | | click 8.1.7 src | Python | 512 | 512 | | | marshmallow 3.21.3 src | Python | 254 | 254 | | | express 4.21.0 lib | JavaScript | 153 | 152 | 1 function lizard runs on past its end into the next one | | axios 1.7.7 lib | JavaScript | 285 | 270 | regex ?, multi-line arrow bodies, lizard ranges | | zod 3.23.8 src | TypeScript | 440 | 413 | ??, )?., multi-line arrow bodies, lizard ranges | | hono 4.6.3 src | TypeScript | 728 | 610 | the same, plus conditional types lizard reads as functions |

Python agrees on every function lizard reads correctly. lizard also leaves out one-line stubs (def f(x) -> T: ...), which Rerail scores 1. Every TypeScript and JavaScript difference inspected traced to one of the deviations above, to lizard getting a function's range wrong (running past its end, ending it early, or missing a function inside an array literal), or to lizard reading a type as a function; none was a Rerail miscount. Not every difference was inspected one by one.

Every file of hono 4.6.3's src parses. Where a file has syntax errors, the functions outside the error regions are scored and the rest are left unknown, so they never report a guessed number.

Parity corpus

upstream/lizard/fixtures/{python,typescript,javascript}/ holds small source files that cover every counted decision, a function at exactly 6 and one at 7 for several kinds, nested functions, Python lambdas and comprehensions, match/case and switch/case, TypeScript optional parameters and properties, every deviation above, files with only simple functions, and test files that lizard scores over the limit. Rerail wrote them; nothing is copied from lizard (upstream/lizard/UPSTREAM.md). upstream/lizard/lizard-1.24.0.tsv records lizard's score for every function (path relative to the corpus, function name as lizard prints it, start line, CCN).

test/engine/code-rule/complexity-rules.test.ts evaluates the three rules over the corpus through the native runtime and checks that:

  • each rule reports exactly the in-scope functions lizard scores above 6, with the recorded number in the message;
  • the same counting without the threshold gives every in-scope function the recorded number, apart from the deviations, which the test lists with lizard's number and Rerail's;
  • test files produce no finding, even though lizard scores them over the limit.

lizard is never a runtime dependency; it runs only to derive the TSV. The corpus and the TSV are pack test data, not in git (CONTRIBUTING.md): the corpus is authored and the TSV is derived from it, and both are the tool-pack's archive in S3, which upstream/lizard/test-data.lock names and npm run fixtures:pull fetches. The test fails saying so without them.

Regenerating the scores

After changing the corpus, derive the TSV again (needs uv on PATH):

npm run fixtures:derive -- --pack complexity

Its recipe runs upstream/lizard/generate.mjs, which runs uvx --from lizard==1.24.0 lizard --csv with and without -m on upstream/lizard/fixtures/, keeps the lower of the two counts for each function, and rewrites upstream/lizard/lizard-1.24.0.tsv, which is never committed. Check the changed corpus files into your pull request with git add -f; a new one needs its row in upstream/lizard/UPSTREAM.md, which is what makes it authored, and removing a corpus file is removing its row. Before merge a maintainer derives the TSV, away from any credential, and archives both from a trusted worktree of main (npm run fixtures:push -- --from <ref>; CONTRIBUTING.md); CI then derives the TSV again and compares it with the archive (Test data reproduces from the pins). A new case lizard mis-scores needs an entry in the test's DEVIATIONS list and a line under "Deviations" above.

Rule details

What each rule matches beyond its one-line description: exact matching, edge cases, upstream settings and known divergences.

complexity-cyclomatic-typescript

Conditional types (T extends U ? X : Y) do not count as decision points. The README's "What counts" list for TypeScript names optional parameters/properties, optional chaining, default parameters, ?? and ??= as not counting, but does not say conditional types are excluded too (it only mentions lizard reading conditional types as functions in the hono comparison).