Problem
New rules must follow a set of conventions, but there is nothing enforcing them automatically:
- Always registered via
conditionalTags, never directly in rules:
- Default value in
extension.neon must be false (opt-in)
- Must be enabled in
bleedingEdge.neon while opt-in
These are easy to miss, as demonstrated by entityStorageDirectInjectionRule shipping without being in bleedingEdge.neon.
Goal
All rules should be toggleable. Going forward, a rule graduates to the default ruleset by flipping its extension.neon default from false to true — it stays in conditionalTags and remains opt-out. Direct rules: registration (not toggleable) is reserved for the existing legacy rules and must not grow.
Proposed solution
Add a PHPUnit test (e.g. tests/src/RuleConventionsTest.php) that parses rules.neon, extension.neon, and bleedingEdge.neon using nette/neon (already available as a transitive dependency) and asserts:
For all rules in conditionalTags:
- A corresponding parameter exists in
extension.neon › parameters › drupal › rules
- If the default is
false, the rule must also be enabled in bleedingEdge.neon
For rules in rules: directly:
- The class must be on a fixed allowlist of pre-existing legacy rules
- Any class not on the allowlist fails the test, preventing new always-on, non-toggleable rules from being added
Convention summary
| Location |
Default in extension.neon |
Meaning |
Required in bleedingEdge.neon |
conditionalTags |
false |
Opt-in, not yet default |
Yes |
conditionalTags |
true |
Graduated to default, still toggleable |
No |
rules: directly |
— |
Legacy, not toggleable — no new additions |
— |
The test always runs — it is cheap and passes silently until a convention is violated.
Implementation prompt
Implement `tests/src/RuleConventionsTest.php` in the phpstan-drupal project.
The test parses three neon files using `Nette\Neon\Neon::decode()` (available via the
phpstan/phpstan phar's bundled nette/neon, or directly from the nette/neon package in vendor):
- `rules.neon` — service definitions and conditionalTags for all rules
- `extension.neon` — parameter defaults under `parameters.drupal.rules`
- `bleedingEdge.neon` — opt-in overrides
The test class should extend `PHPUnit\Framework\TestCase` and contain three test methods:
1. `testNewRulesAreNotDirectlyRegistered`
Parse the `rules:` list from `rules.neon`. Assert that every class in that list is present
in a hardcoded allowlist of legacy always-on rules. Fail with a clear message if a class
not on the allowlist is found — this prevents new non-toggleable rules from being added.
2. `testConditionalRulesHaveFalseDefaultInExtensionNeon`
Collect every class key from `conditionalTags` in `rules.neon` that maps to a
`phpstan.rules.rule` tag. Resolve the parameter name from the tag value
(e.g. `%drupal.rules.fooBarRule%` → `fooBarRule`). Assert that each parameter exists
under `parameters.drupal.rules` in `extension.neon` with a value of `false`.
3. `testOptInRulesAreEnabledInBleedingEdge`
Using the same set of conditional rules, check the default in `extension.neon`. For any
rule whose default is `false`, assert it is present in `bleedingEdge.neon` under
`parameters.drupal.rules` with a value of `true`. Rules whose default is `true`
(graduated) are skipped — they are already on by default and do not need to be in
bleedingEdge.neon.
The neon files are at the project root. Resolve their paths relative to the test file using
`__DIR__` (e.g. `__DIR__ . '/../../../rules.neon'`).
Note: `tests/fixtures/config/phpunit-drupal-phpstan.neon` includes `bleedingEdge.neon`, so
all opt-in rules are active during the full test suite — the conventions test does not need
to load any fixture config, it only reads the neon files as raw data.
Problem
New rules must follow a set of conventions, but there is nothing enforcing them automatically:
conditionalTags, never directly inrules:extension.neonmust befalse(opt-in)bleedingEdge.neonwhile opt-inThese are easy to miss, as demonstrated by
entityStorageDirectInjectionRuleshipping without being inbleedingEdge.neon.Goal
All rules should be toggleable. Going forward, a rule graduates to the default ruleset by flipping its
extension.neondefault fromfalsetotrue— it stays inconditionalTagsand remains opt-out. Directrules:registration (not toggleable) is reserved for the existing legacy rules and must not grow.Proposed solution
Add a PHPUnit test (e.g.
tests/src/RuleConventionsTest.php) that parsesrules.neon,extension.neon, andbleedingEdge.neonusingnette/neon(already available as a transitive dependency) and asserts:For all rules in
conditionalTags:extension.neon › parameters › drupal › rulesfalse, the rule must also be enabled inbleedingEdge.neonFor rules in
rules:directly:Convention summary
extension.neonbleedingEdge.neonconditionalTagsfalseconditionalTagstruerules:directlyThe test always runs — it is cheap and passes silently until a convention is violated.
Implementation prompt