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

phpcognit

v0.2.1

Published

Cognitive complexity linter for PHP, as a single static binary

Readme

phpcognit

Cognitive complexity linter for PHP, shipped as a single static binary.

CI npm dependencies Rust 1.90+ License: MIT

Cognitive complexity measures how hard code is to read, where cyclomatic complexity measures how hard it is to test. A switch with twenty arms is cyclomatically awful and cognitively fine; three nested ifs are the reverse. The metric is SonarSource's.

No PHP runtime, no Composer entry, no PHPStan. The scanned code is never executed — analysis is syntax-only, so it is safe to point at third-party source.

TomasVotruba/cognitive-complexity and Artemeon/cognitive-complexity are PHPStan extensions, Rarst/phpcs-cognitive-complexity is a PHP_CodeSniffer sniff, and ncac/php-cognitive-complexity is a Composer CLI. All of them run inside the PHP process of the project being analysed. This one doesn't:

  • No PHP-version coupling. One pinned binary scores PHP 7.x and 8.x alike, so a monorepo running several versions needs one tool rather than one per service.
  • Nothing added to the target repo. No composer require --dev, no lockfile churn, no version conflict with the project's own PHPStan or PHP_CodeSniffer.
  • Never executes what it scans. No autoloader, no reflection.
  • Right-sized. Cognitive complexity is purely syntactic — nesting and operator sequences, no type resolution. A full semantic engine is more machinery than the metric needs.

Measured against 6,266 files / 613,458 lines of a real production PHP codebase, on an M-series Mac. Reproduce with the commands in benchmark/.

Scores. Implementations of this metric do not all agree, so these cases have answers the specification determines. a && b && c scoring one increment and a && b || c scoring two are worked examples from SonarSource's own paper.

| Case | Spec | phpcognit | ncac | Rarst | TomasVotruba¹ | | --- | --- | --- | --- | --- | --- | | if/if/if nested | 6 | 6 | 6 | 6 | 6 | | if/elseif/else | 3 | 3 | 3 | 3 | 3 | | $a && $b && $c (one run) | 2 | 2 | 2 | 2 | 3 | | $a && $b \|\| $c (two runs) | 3 | 3 | 3 | 3 | 2 | | two sibling ifs at depth 2 | 9 | 9 | 9 | 9 | 7 |

¹ Artemeon/cognitive-complexity is a fork of this package — same file tree, same class names — and returns identical numbers, so the two are one implementation rather than two data points.

Speed.

| Tool | Time | Also does | | --- | --- | --- | | phpcognit | 1.54s | — | | ncac/php-cognitive-complexity | 3.31s | baselines, several report formats | | Rarst/phpcs-cognitive-complexity | 9.63s | runs inside an existing PHP_CodeSniffer setup | | tomasvotruba/cognitive-complexity | 12.77s | full PHPStan analysis — types, reflection |

Read the speed column with care. Only the complexity rule was enabled for the PHPStan run, but a semantic engine still has to boot, autoload and reflect; it is answering a harder question than the others. And 2.1× against ncac is a narrow margin for a native binary versus PHP — that one is fast.

The gap worth attention is the scores column, not the clock. Three implementations built on three different parsers agree; one lineage differs on operator runs and on sibling statements at depth.

Install

brew install ryckakas/tap/phpcognit    # macOS and Linux
npx phpcognit --over 15 src/           # or no install at all
powershell -ExecutionPolicy Bypass -c "irm https://github.com/ryckakas/phpcognit/releases/latest/download/phpcognit-installer.ps1 | iex"
npm install -g phpcognit
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/ryckakas/phpcognit/releases/latest/download/phpcognit-installer.sh | sh

Or take the archive for your platform from Releases, check it against the published SHA-256, and put phpcognit on your PATH. Builds cover macOS (Apple Silicon and Intel), Linux (x86-64 and arm64), and Windows (x86-64).

Usage

phpcognit src/                  # fail on anything above 15
phpcognit --over 10 src/        # stricter
phpcognit --all src/            # every function, ranked
phpcognit --format json src/    # for editors and CI

Output is score path:line Class::method, worst first. A clean run prints nothing and exits 0, so stdout stays usable in a pipeline.

Exit code is 1 on a breach — and also when the scan itself could not be trusted: an unreadable path, or paths matching no PHP at all. A gate that silently passes because a directory was renamed is worse than no gate.

The exit code still reflects the threshold, so a consumer that only wants the data should read stdout and ignore the exit status.

{
  "threshold": 40,
  "breaches": 1,
  "findings": [
    {
      "path": "src/Controller/Component/FormSecurityComponent.php",
      "line": 135,
      "name": "FormSecurityComponent::getFormAccessibleFields",
      "score": 68
    }
  ]
}

Adopting on an existing codebase

Any codebase predating the tool has violations — one we tested against had 180. Nobody refactors 180 functions to adopt a linter, so record them and gate on regressions:

phpcognit --write-baseline src/    # records today's findings, exits 0
phpcognit src/                     # fails only on new or worsened functions

Commit .phpcognit-baseline.json; it is picked up automatically wherever it exists, so CI, hooks and your terminal agree without repeating flags.

Entries are keyed by function name, not line, which matters more than it sounds: a grandfathered file does not become a hiding place. Add a complex new method to an already-recorded file and it is reported, while the old ones around it stay accepted. There is deliberately no ignore-by-file option — the accepted set stays a dated, reviewable list rather than a glob that quietly widens.

Suppressing one function

Some code is irreducibly branchy, and regenerating the whole baseline to accept one deliberate case would re-record every other drift with it. Mark that function instead:

// phpcognit-ignore: dispatch table; splitting it would obscure the mapping
public function dispatch(string $event): void

The reason is mandatory. A bare // phpcognit-ignore is refused, not obeyed — the finding still reports and stderr says why. Suppression stays a decision someone wrote down and a reviewer can see.

Above the declaration, inside its docblock, or trailing the signature line; attributes in between are stepped over.

public function dispatch(string $event): void // phpcognit-ignore: flat dispatch table

It has to be on the signature. A marker written inside the body is not a suppression, so one comment can never silence the function it sits in.

Suppression hides a finding; it never changes a score. --all still shows the real number.

Three rules from the specification: shorthand that doesn't break reading flow is free (?? scores nothing); +1 for each break in linear flow; +nesting when a flow-breaker sits inside other flow-breakers.

| Construct | Increment | Raises nesting | | --- | --- | --- | | if, ternary | +1 +nesting | yes | | elseif, else | +1 flat | yes | | switch, match | +1 +nesting (not per arm) | yes | | for, foreach, while, do | +1 +nesting | yes | | catch | +1 +nesting | yes | | try, finally | — | no | | break N, continue N, goto | +1 | no | | Sequence of like boolean operators | +1 per run | no | | Direct recursion | +1 | no | | Closure, arrow fn, nested function | — | yes |

elseif takes a flat increment deliberately: a long chain reads linearly, so penalising it for depth would misrepresent it.

Boolean operators cost per run, not per operator — the cost is in the switching:

$a && $b && $c              // +1  one run
$a && $b || $c              // +2  two runs
$a && $b && $c || $d || $e  // +3  three runs
$a && ($b || $c)            // +2  parentheses start a fresh run

PHP specifics the specification predates:

  • match (8.0) is treated as switch: one increment for the whole expression.
  • and / or normalise onto && / || for run-counting; xor is its own operator.
  • elseif and else if score identically, despite different parse shapes.
  • break N / continue N are PHP's analogue of the labelled break.
  • Recursion is detected only through direct syntactic self-reference (f(), $this->f(), self::f()). Dispatch through a variable needs symbol resolution and is not guessed at.
src/
├── complexity.rs   the scorer: parsed tree in, scores out. no I/O, no config
├── baseline.rs     recorded scores, and what counts as a regression
├── kinds.rs        every grammar node kind string, in one place
├── lib.rs          public API — the scorer is embeddable
└── main.rs         CLI: file discovery, ranking, exit codes
tests/
├── spec.rs         conformance table against the specification
├── baseline.rs     grandfathering, including the new-function-in-old-file case
├── suppression.rs  marker parsing, and that a marker cannot leak past its function
├── cli.rs          the exit-code contract, including the silent-pass cases
└── grammar.rs      fails if a grammar upgrade renames a node out from under us

complexity.rs is deliberately free of filesystem and CLI concerns, so it can be unit tested directly and reused as a library.

Building needs Rust 1.90 or newer via rustup — a floor set by tree-sitter-language, not by this crate, and one CI builds against on every pull request so the number stays honest.

cargo build --release
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features

Lint configuration lives in Cargo.toml under [lints] rather than #![deny] attributes, so editors, cargo build and CI all see the same rules. Tests run on Linux, macOS and Windows — the matrix is testing the product claim, not decorating a badge.

Releasing is dist: pushing a v* tag builds every target, generates the installers and publishes a GitHub Release. Preview with dist plan. .github/workflows/release.yml is generated — edit dist-workspace.toml and re-run dist generate rather than hand-editing it.

Licence

MIT