Download the PHP package ianrodrigues/pest-plugin-code-quality without Composer

On this page you can find all versions of the php package ianrodrigues/pest-plugin-code-quality. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package pest-plugin-code-quality

Code Quality for Pest

Tests

A third-party Pest plugin, that adds maintainability limits — per method, cyclomatic complexity, body line count, parameter count, name length and longest variable name; per class, declared methods, declared properties, inheritance depth, body line count and name length — to Pest's arch() chain. It is not part of Pest itself, and it ships baselines so a limit can be adopted on a codebase that does not meet it yet.

Requirements

Installation

Nothing else to wire up: the plugin registers its arch() expectations and its CLI options as soon as vendor/bin/pest boots.

On a Laravel application that still lists phpunit/phpunit in require-dev (the default skeleton pins PHPUnit 12), remove that line first so Pest 5 can bring PHPUnit 13: composer remove --dev phpunit/phpunit.

Quick start

Ten expectations join Pest's arch() chain, each taking an inclusive limit — AtMost(10) passes a symbol that measures exactly 10:

Expectation Metric Measured per
toHaveMethodComplexityAtMost(int $max) ccn2 method
toHaveMethodLinesAtMost(int $max) lines method
toHaveMethodParametersAtMost(int $max) params method
toHaveMethodsAtMost(int $max) methods class
toHavePropertiesAtMost(int $max) properties class
toHaveInheritanceDepthAtMost(int $max) inheritance class
toHaveClassLinesAtMost(int $max) classLines class
toHaveClassNamesAtMost(int $max) className class
toHaveMethodNamesAtMost(int $max) methodName method
toHaveVariableNamesAtMost(int $max) variableName method

A policy for controllers, and a separate complexity budget for a parsing layer:

Run the suite. A method over budget fails with the offending symbol, its location, how far past the limit it is, and what was counted — this is the real output of the policy above against a CheckoutController::store that has grown too many branches:

The expectations target whatever the rest of the chain targets: a namespace, an exact class, an array of either, combined with classes(), enums(), traits() and the other built-in filters.

Expectations reference

Every expectation takes:

toHaveMethodsAtMost() takes one more, between the two:

A class-scoped expectation names the class in its failure, and points at the class declaration:

They compose with each other and with built-in arch expectations, in either order, on the same chain:

Excluding known offenders keeps a policy honest about what it does not yet cover, instead of quietly loosening the limit:

ignoring() takes a class, a namespace, or an array of either. test()->arch()->ignore([...]) does the same for every expectation in the test. A single method can opt out at its declaration, the same way it would for a built-in arch expectation:

@pest-arch-ignore-next-line on the line before the declaration works the same way, for a docblock that already carries something else.

What Counted lists

A failure on ccn2, methods, properties or inheritance ends with a Counted: block: the items that produced the value.

A list of more than 10 declarations or parents stops there and points at the JSON report, where every violation carries the full list as contributions[]. The other metrics have nothing to list: lines, params, classLines and the three name lengths are the value itself.

not is unsupported

->not->toHaveMethodComplexityAtMost(10) throws IanRodrigues\CodeQuality\Exceptions\UnsupportedModifier rather than accepting a nonsensical query: a numeric limit has no negation. Lower the limit instead. A negative limit throws IanRodrigues\CodeQuality\Exceptions\InvalidLimit where the expectation is declared, before anything runs.

Adopting on an existing codebase

A limit nobody meets yet is a limit nobody adds. A baseline records what every method already measures, so a policy can go in today, pass on the code as it stands, and fail only when something gets worse.

  1. Add the policy at the limit you actually want.
  2. Run the suite to see what stands between the codebase and that limit. With no baseline configured yet, every offending method is listed as an ordinary failure, with how far past the limit it goes.
  3. Point the plugin at a baseline file — the one thing configured outside the arch() chain, since pest() returns a final object with no room for the plugin's own accessor — and generate it:

    Generating tolerates a baseline file that does not exist yet — that run is what creates it. It writes one entry per method above the limit and prints the diff — added / removed / increased / decreased. The file is the list of everything being accepted for now; review it like any other change. It needs a complete, error-free run: if any policy errored (an unreadable file, an empty selection, a skipped file under Config::strict()), nothing is written, because the missing policy's methods would silently be accepted.

    A chain of several limits on one arch() call — ->toHaveMethodComplexityAtMost(10)->toHaveMethodLinesAtMost(40)->toHaveMethodParametersAtMost(4) — records every one of them in this single run, not only the first that finds something to accept. The generating run itself still exits non-zero over code that still breaks a limit; the normal run right after it is the one that passes.

  4. Commit the policy and the baseline together. Apart, neither means anything: the baseline is the evidence for what the policy is allowed to ignore.
  5. From then on, normal runs fail on regressions only. A method already in the baseline may stay as bad as it was; one growing worse fails, and so does a new method over the limit that the baseline never saw.

A failure against a raised ceiling spells out what was accepted and how much was added on top — here, Invoice::total was baselined at 14 and has grown one branch further:

--quality-baseline=path/to/baseline.json configures the file for one run, overriding Config::baseline(). A baseline that is configured but missing or malformed is an error, not an empty baseline: silently accepting nothing would turn a typo into a green suite.

Tightening instead of accepting forever

Lowers accepted values to what the run actually measured, and drops entries that are back within the limit. It never raises a value and never adds one, so it can only give allowance back — it has the same completeness requirement as --quality-baseline-generate. Once Invoice::total above is brought back down to 12, still over the limit but under what was accepted, tightening prints:

Both commands work under --parallel: every worker hands its measurements back to the process that owns the output, which writes the file once.

What stale means

An entry stores the metric version and the limit it was measured under. If either changes, the entry no longer describes anything true, so it is marked stale: ignored when checking, and listed in the run summary and in --quality-inspect.

Regenerating clears stale entries. Tightening leaves them alone — there is nothing to lower a value to when the value was measured against a different rule.

How an entry finds its policy

An entry is filed under a policy's identity, which is:

Neither the declaring file nor the line takes part, so moving or reordering a test file keeps every entry valid. The limit takes no part either, so changing it marks entries stale rather than orphaning them. Two policies that resolve to the same identity are a configuration error naming both declaration sites — give the tests different descriptions.

Entries are filed per symbol, so a renamed method is a new method: it has no entry and no allowance. A removed method leaves an entry behind that never fails anything and that --quality-baseline-tighten prunes.

The file shape is versioned at schema/quality-baseline.v1.json: schemaVersion, generatedAt, pluginVersion, and entries[], each with policy, symbol, metric (name, version), limit, accepted and a diagnostic path. Only values above the limit are stored, sorted by policy then symbol, so two runs over the same code write the same bytes.

Inspection and JSON report

Two read-only CLI options show what a policy actually selected and measured, even when everything passes — they never change a test's outcome or the run's exit code, and both work under --parallel.

Prints, for every recorded policy, its test name and file:line, targets, directories searched, exclusions applied, completeness counts, skipped files with reason, and a table of every measured method sorted by path then symbol — this is the real output for the two policies above:

--quality-inspect=path/to/report.json writes the same data as JSON to that path instead of printing it, against the schema at schema/quality-report.v1.json (JSON Schema, draft 2020-12).

Writes only findings — violations, errors, and completeness counts — without the per-method measurements, for a smaller report. Same schema, measurements omitted.

Both JSON documents carry schemaVersion, generatedAt, versions (php, pest, plugin), a policies[] list each with id, location, targets, metric, limit, coverage, violations[], errors[], and a top-level truncated: false. A violation row carries symbol, path, line, value, limit and contributions[]: the items a failure lists under Counted, each with a label and a line (null for a parent), and an empty list for a metric that has none. A policy run with a baseline configured also carries a baseline block: its path, how many entries applied, and the stale[] ones.

Selection and completeness

Pest's architecture layer resolves a namespace to PSR-4 directories, then reflects each file it finds; a file that fails to autoload, or whose class lives in a different namespace than its directory implies, is silently dropped by Pest. A policy can then "pass" having measured nothing. This package tracks that instead of trusting it — exposed on the arch expectation's result as IanRodrigues\CodeQuality\Selection\Coverage: PHP files found, objects produced, objects with an AST, and eligible symbols measured.

Empty selections error by default

If a target selects no classes that can be measured, the test errors with IanRodrigues\CodeQuality\Selection\EmptySelection, naming the target, the directories searched, and how many files and objects were found. This is deliberate: an empty selection almost always means a namespace typo or a directory nobody wired up. A selection that holds only classes without method bodies (an abstract base, an interface) passes with nothing measured, which --quality-inspect shows as Methods measured: 0.

When it is genuinely expected — a namespace still being scaffolded, say — opt out per expectation:

Skipped files warn, unless strict

A file found under a target's directories that never became a measurable object is skipped, not silently dropped: the policy still runs, and the result carries the file with a reason (not loadable, namespace mismatch, vendor, or no ast). By default these are warnings, printed once per process after the run:

IanRodrigues\CodeQuality\Config::strict(true) turns skipped files into an error (IanRodrigues\CodeQuality\Selection\SkippedFilesFound) instead of a warning. It is a static, resettable switch — call Config::reset() to return to warning. There is no CLI flag for it yet.

A file that declares no class, interface, trait or enum — a functions file, a config file returning an array — is not a skip either: there is nothing to measure, so it counts in filesFound only, shown by --quality-inspect as Files without classes: N.

Vendor code cannot be analysed

A target that resolves entirely under vendor/ errors with IanRodrigues\CodeQuality\Selection\VendorTarget: Pest's architecture layer never produces an AST for vendor code, so there is nothing to measure there — allowEmpty does not apply, because the problem is not an empty result, it is an impossible one.

Metric definitions

Every measurement is tagged with a {name, version} identity, for example ccn2@1IanRodrigues\CodeQuality\Metrics\Metric. Changing a definition below bumps the version rather than silently reinterpreting existing baselines. ccn2, lines, params, methodName and variableName are measured per method; methods, properties, inheritance, classLines and className are measured per class, interface, trait and enum.

A name length is always a character count, taken with mb_strlen(): a multibyte identifier counts by character, never by byte. Line length itself is out of scope for this package; keep that with a formatter, such as Pint's line-length rules.

ccn2 v1 — method cyclomatic complexity

Starts at 1. The following each add 1, wherever they occur inside the method's body — including inside closures and arrow functions declared within it, since those contribute to the enclosing method rather than being measured on their own:

Construct Example Effect
if if ($x) { ... } +1
elseif if ($x) { ... } elseif ($y) { ... } +1 for the elseif
for for ($i = 0; $i < 10; $i++) { ... } +1
foreach foreach ($items as $item) { ... } +1
while while ($x) { ... } +1
do do { ... } while ($x); +1
case (not default) switch ($x) { case 1: ...; } +1 per non-default case
catch try { ... } catch (Throwable $e) { ... } +1 per catch block
ternary ? : $x ? 'a' : 'b' +1
short ternary ?: $x ?: 'default' +1
&& $a && $b +1
\|\| $a \|\| $b +1
and $a and $b +1
or $a or $b +1
xor $a xor $b +1
?? $a ?? $b +1
??= $a ??= $b +1
?-> $a?->b or $a?->b() +1
match arm (not default) match ($x) { 1 => 'a', default => 'b' } +1 for the 1 => arm only

else, finally, try, break, continue, return, throw, the switch statement itself, and the match expression itself do not add to ccn2. A match arm with several comma-separated conditions (1, 2 => 'a') still counts once, as one arm.

A worked example, exercising several constructs at once:

lines v1 — method body lines

The count of physical lines strictly between the method's opening and closing brace that contain at least one PHP token other than whitespace or a comment. A physical line whose only non-whitespace, non-comment content is one or more {/} characters does not count — this includes any standalone brace line closing a nested block. Several statements on one physical line count once. Every physical line occupied by a multiline string or heredoc counts, including its opening and closing lines.

params v1 — declared parameters

Every declared parameter is counted once, regardless of whether it is required, optional (has a default), promoted (constructor property promotion), passed by reference, or variadic.

methods v1 — declared methods

Every method the declaration itself holds, counted once. A constructor counts as one method whatever it promotes. A method reached through extends belongs to the class that declares it, and a method reached through use belongs to the trait that declares it; neither is counted here.

With ignoringAccessors: true, a method is dropped from the count when its body is exactly one return $this->property;, or exactly one assignment to $this->property — whatever value it assigns — followed by nothing or by return $this;. The default counts them, so the noise is opt-out and visible in the policy.

properties v1 — declared properties

Every property the declaration itself holds. One declaration listing several names counts once per name, and a promoted constructor parameter counts as the property it promotes. A constant is not a property, an enum case is not a property, and a property reached through extends or use belongs to the declaration it comes from.

inheritance v1 — parents up to the root

The number of classes between the class and the root of its hierarchy. A class that extends nothing measures 0, a class that extends one class measures 1, and so on. An implemented interface is not a parent, and neither is a used trait. A parent this package never analyses — one in vendor/, or one PHP itself ships — still counts, and so does its own chain, read by reflection.

classLines v1 — class body lines

The count of physical lines strictly between the declaration's opening and closing brace that contain at least one PHP token other than whitespace or a comment, counted exactly as lines v1 counts a method body. A nested declaration's lines count too, since they sit inside those braces.

className v1 — class name length

The character count of the declaration's own short name — a class, interface, trait or enum — never its namespace.

methodName v1 — method name length

The character count of the method's own short name. A magic method (__construct, __toString, __get, and the rest PHP reserves a __ name for) is exempt: its name is not the method's own choice to make.

variableName v1 — variable name length

The character count of the longest variable identifier — without its $ — a method declares, counted per declaration site: a parameter (a promoted constructor property included), a local assignment, a foreach key or value, a catch variable, and a closure or arrow function's own parameters and, for a closure, its use variables. $this and a superglobal ($_GET, $GLOBALS, and the rest) are never a declaration and never counted. A method that declares no variable measures 0.

A failure names the longest identifier itself, alongside its length:

Eligibility

Why the same method reports a different number elsewhere

ccn2 counts more constructs than most complexity tools, which is why the same method can measure differently here than it does under another tool:

Tool What it measures Constructs it omits that ccn2 v1 counts
sebastian/complexity (used by PHPUnit's risky-test detection) Cyclomatic complexity per method/function ??, ??=, ?->. For a method whose only counted constructs are those three operators, sebastian_ccn2 = ccn2 - occurrences_of(??, ??=, ?->).
PDepend's ccn2 (extended complexity) Cyclomatic complexity per method/function match arms, ??, ??=, ?-> — its grammar predates these PHP 8 constructs.
PHP_CodeSniffer's Generic.Metrics.CyclomaticComplexity sniff Cyclomatic complexity per function Ternary ? :, short ternary ?:, match, ??, ??=, ?-> — the sniff only walks a fixed set of branching keywords and boolean-operator tokens.

None of this makes one number more "correct" than another; it means a limit tuned against one tool's output is not the same limit under this plugin. Set limits from what ccn2 reports, not from a number carried over from a different tool.

Performance

Measured on a GitHub-hosted ubuntu-latest runner (AMD EPYC 9V74, PHP 8.5.10) against a generated project of 350 files and 51,140 lines, three policies on one namespace, median of three fresh processes, commit c4c8468.

Scenario Wall time Peak memory
Measurement only, one process 0.65 s 64 MB
Full Pest run, no baseline 1.31 s 185 MB
Full Pest run, baseline configured 1.30 s 181 MB
Full Pest run, --parallel 1.55 s 180 MB

There is no persistent cache yet, so a second run costs the same as the first. Reproduce locally with composer perf, or on GitHub through the manual Performance workflow, which prints the same table in the job summary.

Limitations

Versioning

This package follows Semantic Versioning. Independently of the package version, each metric definition (ccn2, lines, params, methods, properties, inheritance, classLines, className, methodName, variableName) carries its own version, stamped on every measurement and stored in every baseline entry — see Requirements, and the PHP/OS matrix CI runs against — are documented per release in CHANGELOG.md as well.

Development

Individual steps: composer lint (fix formatting), composer lint:check (verify only), composer analyse (PHPStan), composer rector (apply Rector) and composer rector:check (dry run), composer test (Pest), composer test:parallel (Pest, parallel). See CONTRIBUTING.md for the fixture and commit conventions, and how to add a metric or a CLI option.

composer install also points git at .githooks/: pre-commit lints staged PHP, commit-msg validates the commit message format. CI runs composer lint:check, composer analyse, composer rector:check, composer test and composer test:parallel on the PHP/OS matrix in .github/workflows/tests.yml.

PHPStan (level max) sees every expectation through extension.neon, picked up automatically by phpstan/extension-installer or added to includes by hand. Editors that do not run PHPStan can be pointed at stubs/expectations.stub.php, which declares the same methods as @method annotations for autocompletion.

License

MIT. See LICENSE.


All versions of pest-plugin-code-quality with dependencies

PHP Build Version
Package Version
Requires php Version ^8.4
pestphp/pest-plugin Version ^5.0
pestphp/pest-plugin-arch Version ~5.0.0
nikic/php-parser Version ^5.9
symfony/finder Version ^7.0 || ^8.1.5
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package ianrodrigues/pest-plugin-code-quality contains the following files

Loading the files please wait ...