techno-artisan / phpstan-strict-rules
Additional strict rules for PHPStan.
Package info
github.com/techno-artisan/phpstan-strict-rules
Type:phpstan-extension
pkg:composer/techno-artisan/phpstan-strict-rules
Requires
- php: ^8.5
- phpstan/phpstan: ^2.2
Requires (Dev)
- ergebnis/composer-normalize: ^2
- infection/infection: ^0.29
- nette/neon: ^3
- phpunit/phpunit: ^12
This package is auto-updated.
Last update: 2026-07-26 13:52:08 UTC
README
Uncompromising, purely-syntactic PHPStan rules — because loose semantics hide bugs.
A small set of additional, deliberately strict rules for PHPStan.
Every line below passes vanilla PHPStan at level: max — and is reported here:
class Config { const TIMEOUT = 30; } // ❌ technoArtisan.typedClassConstant if (empty($user)) { /* … */ } // ❌ technoArtisan.disallowedEmpty if ($id == $request->id) { /* … */ } // ❌ technoArtisan.looseComparison in_array('1', [1, 2, 3]); // ❌ technoArtisan.looseInArray (returns true!)
Why this package?
- Purely syntactic. The rules inspect the AST, not inferred types — so they fire predictably on every occurrence, with no type-inference guesswork.
- Uncompromising. No configuration, no opt-outs: a construct is either allowed or it isn't.
- One thesis. Loose semantics hide bugs — loose
empty(), loose array searches and loose==comparisons are all reported, alongside untyped class constants. - Strict with itself. Enforced in CI: 100 % line coverage, 100 % mutation score
(MSI), and the suite runs against both the lowest and highest supported PHPStan
^2.2release. A rule that isn't bulletproof doesn't ship. See Quality.
Relationship to phpstan/phpstan-strict-rules
phpstan/phpstan-strict-rules is
broader and excellent — if you don't already use it, you probably should. Three of the
four rules here overlap with rules it enables by default:
| Construct | phpstan-strict-rules | this package |
|---|---|---|
empty() |
bans all (DisallowedEmptyRule) |
bans all |
== / != / <> |
bans all (DisallowedLooseComparisonRule) |
bans all |
in_array / array_search / array_keys w/o $strict |
type-aware, plus base64_decode (StrictFunctionCallsRule) |
purely syntactic — requires the literal true |
| Untyped class constants | — (no equivalent) | TypedClassConstantRule |
What this package adds on top:
TypedClassConstantRule— strict-rules has no equivalent.- A purely syntactic loose-array-search check. Because it never consults inferred
types, it also flags
in_array($x, $list, $flag)when$flagis only inferred to betrue— predictable, no guesswork. (strict-rules is type-aware and additionally coversbase64_decode.)
The empty() and == rules are included so this package stands on its own. If you run
both packages, those two report the same lines twice — disable them on one side:
# phpstan.neon — turn off the duplicates in phpstan-strict-rules parameters: strictRules: disallowedEmpty: false disallowedLooseComparison: false
(or ignore the technoArtisan.* identifiers from this package instead).
Installation
composer require --dev techno-artisan/phpstan-strict-rules
Usage
If you use phpstan/extension-installer,
the rules are registered automatically — nothing else to do.
Otherwise, include the rule set manually in your phpstan.neon:
includes: - vendor/techno-artisan/phpstan-strict-rules/rules.neon
Rules
| Rule | Reports | Identifier |
|---|---|---|
TypedClassConstantRule |
Class constants declared without a native type | technoArtisan.typedClassConstant |
DisallowEmptyConstructRule |
The empty() language construct |
technoArtisan.disallowedEmpty |
DisallowLooseInArrayRule |
in_array() / array_search() / array_keys() without $strict |
technoArtisan.looseInArray |
DisallowLooseComparisonRule |
The ==, != and <> operators |
technoArtisan.looseComparison |
Typed class constants — TypedClassConstantRule
A class constant without a native type leaves its type implicit. Declare it.
// ❌ reported final class Config { const TIMEOUT = 30; } // ✅ instead final class Config { const int TIMEOUT = 30; }
Identifier: technoArtisan.typedClassConstant
Loose empty() — DisallowEmptyConstructRule
empty() treats 0, 0.0, "0", "", [], null and false alike, so it
silently swallows values you may care about. Use an explicit strict check.
// ❌ reported if (empty($value)) {} // ✅ instead if ($value === null || $value === '') {}
Identifier: technoArtisan.disallowedEmpty
Loose array search — DisallowLooseInArrayRule
in_array(), array_search() and array_keys() compare loosely unless you pass
true as the $strict argument, so they coerce types and hide bugs.
// ❌ reported in_array('1', [1, 2, 3]); // true — '1' is loosely equal to 1 // ✅ instead in_array('1', [1, 2, 3], true); // false
Identifier: technoArtisan.looseInArray
Loose comparison — DisallowLooseComparisonRule
==, != and <> coerce their operands' types and hide bugs. Use === / !==.
// ❌ reported if ($id == $input) {} // 0 == 'foo', '1e1' == '10' and null == false are all true // ✅ instead if ($id === $input) {}
Identifier: technoArtisan.looseComparison
Ignoring a rule
Every error carries a stable identifier (shown above), so you can suppress a single occurrence with an ignore comment:
$x == $y; // @phpstan-ignore technoArtisan.looseComparison
…or ignore it project-wide via ignoreErrors in phpstan.neon or a generated
baseline. Because the rules are uncompromising by design, prefer fixing the code
over ignoring it.
Requirements
- PHP
^8.5 - PHPStan
^2.2
Quality
This package is strict with itself. Every release is gated in CI:
- 100 % line coverage of
src/, enforced bycomposer coverage:check. - 100 % mutation score (MSI) via Infection — covered-MSI is 100 % too. A rule whose tests don't kill every mutant doesn't ship.
- lowest + highest matrix — the suite runs against both the oldest and newest
supported PHPStan
^2.2release, so the rules can't quietly break on either end. - Config hygiene —
composer.jsonstays normalized andrules.neon/phpstan.neonare lint-clean.
These same gates run locally — see Development.
Development
composer install # install dependencies composer test # run the PHPUnit suite composer phpstan # run PHPStan on src/ — dogfoods this package's own rules composer test:coverage # run the suite and write coverage.xml composer coverage:check # fail unless line coverage is 100% composer infection # mutation testing — fail unless MSI is 100% composer lint:neon # validate rules.neon and phpstan.neon composer normalize:check # fail unless composer.json is normalized
License
MIT — see LICENSE.