Search by

steevanb / phpstan-rules

steevanb

PHPStan rules: unnecessary named arguments, one mirrored coverage target per test, no class constants in tests

Package info

github.com/steevanb/phpstan-rules

Type:phpstan-extension

pkg:composer/steevanb/phpstan-rules

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.1.0 2026-10-07 17:18 UTC

This package is auto-updated.

Last update: 2026-10-07 18:09:19 UTC


README

PHPStan rules:

Rule Identifier(s) Purpose
DisallowUnnecessaryNamedArgumentsRule steevanb.unnecessaryNamedArguments A named argument that could be passed positionally is reported
CoverageTargetRule steevanb.singleCoverageTarget, steevanb.mirroredCoverageTarget A test class covers a single class or trait, the one it mirrors
DisallowConstantsInTestsExtension steevanb.disallowConstantsInTests A TestCase uses literal values, not constants of the tested code

Installation

composer require --dev steevanb/phpstan-rules

With phpstan/extension-installer, the rules are registered automatically. Otherwise, include extension.neon in your PHPStan configuration:

includes:
    - vendor/steevanb/phpstan-rules/extension.neon

Configuration

Every parameter is optional:

parameters:
    steevanbPhpStanRules:
        coverageTarget:
            # Namespace prefixes of the test classes, removed to find the mirrored class.
            testNamespacePrefixes:
                - App\Tests\Unit\
                - App\Tests\Functional\
            # Namespace prefixes of the covered classes, the most specific first.
            targetNamespacePrefixes:
                - App\Tests\
                - App\
        disallowConstantsInTests:
            # Constants declared in these directories stay allowed in tests.
            # Relative paths are resolved from the directory PHPStan is launched from.
            # The "!" replaces the default value ("tests") instead of adding to it.
            testsDirectories!:
                - tests

Rules

DisallowUnnecessaryNamedArgumentsRule

Named arguments are only useful to skip optional parameters. When the named arguments of a call could be passed positionally (they directly follow the positional arguments, in the order of the parameters), they are reported.

Method calls, nullsafe method calls, static calls, function calls, new and attributes are checked. Calls using argument unpacking (...$arguments), first-class callables and calls whose callee can't be resolved are ignored.

str_pad('x', 3, pad_type: STR_PAD_LEFT); // ✔ pad_string is skipped
str_pad('x', 3, ' ', pad_type: STR_PAD_LEFT); // ✘ Named argument pad_type of str_pad() is unnecessary
str_pad(length: 3, string: 'x'); // ✘ Named arguments string, length of str_pad() are unnecessary

CoverageTargetRule

A class declaring several #[CoversClass] / #[CoversTrait] attributes is reported: a test class covers a single class or a single trait.

When testNamespacePrefixes and targetNamespacePrefixes are configured, the covered class must be the one the test class mirrors: with the configuration above, App\Tests\Unit\Foo\BarTest must cover App\Tests\Foo\Bar or App\Foo\Bar. Classes outside of testNamespacePrefixes or without the Test suffix are not checked.

namespace App\Tests\Unit\Foo;

#[CoversClass(\App\Foo\Bar::class)] // ✔
final class BarTest extends TestCase {}

#[CoversClass(\App\Foo\Baz::class)] // ✘ covers App\Foo\Baz, a test class covers the class or trait it mirrors
final class BarTest extends TestCase {}

DisallowConstantsInTestsExtension

Inside a PHPUnit\Framework\TestCase subclass, using a class constant of the tested code is reported: a test asserts literal values, so that changing the value of a constant breaks the test.

Enum cases and constants declared in testsDirectories stay allowed.

final class FooTest extends TestCase
{
    public function testType(): void
    {
        static::assertSame(Foo::TYPE, new Foo()->getType()); // ✘ Using App\Foo::TYPE in tests is disallowed
        static::assertSame('foo', new Foo()->getType()); // ✔
    }
}

Development

All the tooling runs inside a single Docker container (ghcr.io/steevanb/phpstan-rules:ci, built from docker/ci/), through the Python wrappers of steevanb/python-devops in bin/. Each wrapper re-runs itself in a docker run of this image, so the host needs only Python 3.10+ and Docker.

# Pull the CI image (--local to keep the local one) and install the dependencies
bin/ci/start.py

# Run every check in parallel
bin/ci/validate.py

# Or run them one by one
bin/ci/phpunit.py
bin/ci/phpstan.py
bin/ci/phpcs.py
bin/ci/phpcbf.py
bin/ci/composer-require-checker.py
bin/ci/composer-normalize.py
bin/ci/composer-validate.py
bin/ci/phpdd.py

# Run any command in the CI container
bin/ci/shell.py sh

# Build the CI image (--push to publish it to ghcr.io)
bin/docker/build.py

steevanb/python-devops is a private repository, declared as a vcs entry in composer.json. Composer needs a GitHub token (classic, repo scope) in ~/.config/composer/auth.json to install it:

{
    "github-oauth": {
        "github.com": "ghp_yourtokenhere"
    }
}

composer.lock is not committed, so bin/ci/start.py mounts this file in the container to install the dependencies.

CI

The GitHub Actions workflow (.github/workflows/ci.yml) runs bin/ci/start.py then bin/ci/validate.py on every push. It needs a GHCR_TOKEN Actions secret, a classic token with the read:packages (pull ghcr.io/steevanb/phpstan-rules:ci) and repo (install steevanb/python-devops) scopes:

https://github.com/steevanb/phpstan-rules/settings/secrets/actions