Search by

stejac / code-thresholds

jacomeit

Dependency-free size and complexity gate for PHP that checks only changed files: method count, parameters, returns, function length, nesting depth, unused variables.

Package info

github.com/Stefan39/code-thresholds

pkg:composer/stejac/code-thresholds

Fund package maintenance!

www.jacomeit.de

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-10-14 11:39 UTC

This package is auto-updated.

Last update: 2026-10-14 11:45:17 UTC


README

A dependency-free size and complexity gate for PHP that checks only the files you changed: method count, parameters, returns, function length, nesting depth and two defect checks, in a pre-commit hook or a pull request.

CI PHPStan level max Latest Stable Version codecov License: MIT

Deutsche Version

Why this one?

Size and complexity limits are usually enforced late: a hosted analysis reports them after the push, and the fix turns into another round of review. This tool moves exactly the limits that can be counted from a single file to the moment before the commit.

Tool What it is good at Where code-thresholds differs
PHPStan Types and bugs It has no size or complexity limits; this tool does not look at types at all. Use both.
PHPMD Similar metrics (excessive method count, parameter list length, ...) PHPMD brings its own dependencies and rule-set XML. This tool is one composer require --dev with ext-tokenizer as its only requirement, and one small PHP config file.
PHP_CodeSniffer Code style, plus a few metrics sniffs Style is not the goal here, and there is no ruleset to compose.
SonarCloud Cognitive complexity, duplication, security hotspots, history It runs on the server after the push. This tool answers locally, in the pre-commit hook or the pull request job, before a finding exists.

What sets it apart is the combination: zero dependencies, only changed files (staged, or a diff against a base ref), so it stays fast enough for every commit, and one config file that is plain PHP.

What it deliberately leaves out

A check that reports something a project does not really enforce is worse than no check: it blocks commits and gets switched off. Three checks are therefore absent on purpose.

  • Cognitive complexity. A token-based reimplementation, compared with reference figures, came out too high for one method and too low for another. Understating complexity is the dangerous direction because it grants false confidence, so this stays with the reviewer or a full analysis tool.
  • Duplicated literals. Counting them flags ordinary array keys such as 'id' or 'value': noise, not signal.
  • Copy-paste detection. It needs a tokeniser built for duplication; a local imitation either misses duplicates or cries wolf. Use a dedicated tool such as jscpd or PHP Copy/Paste Detector.

Everything is measured on token_get_all(), never with regular expressions. A regex over string literals matches the gap between two array keys as if it were a literal of its own.

Quickstart

composer require --dev stejac/code-thresholds
vendor/bin/code-thresholds                 # PHP files staged in git
vendor/bin/code-thresholds --base=origin/main   # files changed against a ref
vendor/bin/code-thresholds --all           # everything below source_dirs
vendor/bin/code-thresholds src/Foo.php lib # explicit files or directories

Without a config file the defaults apply and src/ is the source directory. Output on success goes to stdout, the violation list to stderr:

code-thresholds: limits exceeded:

  src/Service/Importer.php:42
    parameters — import() is 9 (max 7)
  src/Service/Importer.php:57
    unused variable — $legacy in import()

2 violation(s).

Exit codes: 0 no violations, 1 violations found, 2 usage or configuration error.

Ready-made integrations:

Features

Check Config key Default Counts
Methods per class limits.methods_per_class 20 Methods of one class, interface, trait or enum. Closures are not methods.
Parameters limits.parameters 7 Parameters of a function or method. A trailing comma and commas inside default values or attributes are not counted.
Returns limits.returns 3 return statements in a function. Returns inside a nested closure belong to the closure.
Function length limits.function_length 100 Lines of the function body.
Nesting depth limits.nesting_depth 4 Nested if, for, foreach, while, switch, try and match blocks. Ten sequential if blocks are depth one.
Unused variable rules.unused_variable on A variable that appears exactly once in a function body, as an assignment. Deliberately conservative to avoid false alarms with destructuring, references and interpolation.
Loop counter reassigned rules.loop_counter_reassigned on A for counter that is assigned inside the loop body. Use while instead.

Configuration

Create code-thresholds.php in the directory you run the tool from (or pass --config=<file>). It returns an array; every key is optional. Without a file the defaults below apply. Unknown keys and wrong types stop the run with exit code 2.

<?php

return [
    'source_dirs' => ['src', 'lib'],
    'exclude' => ['src/Generated/*'],
    'limits' => ['parameters' => 5, 'function_length' => null], // null switches a rule off
    'rules' => ['loop_counter_reassigned' => false],
];
Key Type Default Meaning
source_dirs list<string> ['src'] Directories scanned by --all. Missing directories are skipped; if none exists the run fails with exit code 2.
exclude list<string> [] fnmatch() globs of files that are never checked. * also matches /.
limits array<string, int|null> see Features Keys methods_per_class, parameters, returns, function_length, nesting_depth. null disables a limit.
rules array<string, bool> all true Keys unused_variable, loop_counter_reassigned.
methods_per_class_exclude list<string> ['*/Entity/*', 'tests/*'] Globs of files exempt from the method count only. Entities track the width of a table, and a test class holds one method per scenario.
exempt_constructors bool true Constructors are not checked against the parameter limit: injecting eight collaborators is not the same defect as an eight-argument call.

The project root is the directory of the config file, or the working directory when there is none. Globs and source_dirs are relative to it. The match is anchored at the root on purpose: a checkout below a directory that happens to be called tests must not exempt all of src/.

Paths that do not exist, files that are not *.php and excluded files are ignored.

Security

The tool reads the PHP files it is pointed at and never executes them. The config file is the exception: it is a PHP file that is included, so treat it like composer.json scripts and only run the tool in projects you trust. Git is started without a shell and the --base ref is validated against a conservative pattern, so a ref cannot inject options or commands. To report a vulnerability, see SECURITY.md.

Requirements

PHP 8.3 or newer with ext-tokenizer. git for staged and --base mode. Developed and tested on Linux and macOS.

Contributing

Issues and pull requests are welcome, see CONTRIBUTING.md. The checks you need locally:

composer test         # PHPUnit
composer phpstan      # PHPStan, max level
composer cs           # php-cs-fixer (dry run)
composer thresholds   # the package checks its own src/ with itself

About

Built and maintained by Stefan Jacomeit.

License

MIT, see LICENSE.