Skip to content

fix: declare nikic/php-parser and make the CI matrix real - #11

Merged
MrPunyapal merged 1 commit into
5.xfrom
fix/php-parser-floor
Oct 9, 2026
Merged

MrPunyapal merged 1 commit into
5.xfrom
fix/php-parser-floor

Conversation

@MrPunyapal

Copy link
Copy Markdown
Member

Follow-up to #10. That PR fixed the abandoned symplify/rule-doc-generator-contracts dependency correctly, but it also widened the args annotations to accept PhpParser\Node\ArgPlaceholder, which introduced a problem on the lowest supported dependency set.

The bug

nikic/php-parser was never declared in require. src/ imports 71 distinct PhpParser\* symbols across 465 references, and the package only resolved because phpunit/phpunit happened to allow it. The version was never ours to control.

ArgPlaceholder was added in php-parser 5.9.0. On prefer-lowest, composer resolves 5.8.0, where that class does not exist:

prefer-lowest -> nikic/php-parser 5.8.0
vendor/nikic/php-parser/lib/PhpParser/Node/ArgPlaceholder.php -> does not exist
composer test:types -> 23 errors   (20 class.notFound, 3 argument.type)

Why CI did not catch it

static.yml declared a prefer-lowest matrix entry but hardcoded the install:

dependency-version: [prefer-lowest, prefer-stable]
...
run: composer update --prefer-stable --no-interaction --no-progress --ansi

Both legs installed the same dependency set, so PHPStan only ever saw 5.9.0 where the fix is valid. The CI log confirms it, the job named prefer-lowest locks nikic/php-parser (v5.9.0). Meanwhile tests.yml does honour the matrix and its prefer-lowest leg genuinely installs 5.8.0, but it only runs composer test:unit, and the references are docblock-only with no runtime effect, so tests pass either way.

Nothing was checking the dependency set where the code actually breaks. Fixing the matrix is the durable part here, since it makes this whole class of undeclared dependency visible automatically from now on.

Changes

  • composer.json: declare "nikic/php-parser": "^5.9"
  • static.yml: install with ${{ matrix.dependency-version }}
  • static.yml: add a composer audit --locked step, so an abandoned transitive dependency fails CI rather than reaching a release

Verification

Both matrix legs, at the corner CI actually resolves (rector 2.7.0, phpstan 2.3.0):

prefer-lowest prefer-stable
composer test:types 0 errors 0 errors
composer test:unit 290/290 290/290
composer audit clean clean
composer validate --strict valid valid

Also checked a prod-only consumer (composer update --no-dev, plugin as the only requirement):

symplify/rule-doc-generator-contracts   absent
nikic/php-parser                         5.9.0
phpstan/phpstan                          2.3.0  (transitive via rector)
PhpParser\Node\ArgPlaceholder            resolvable
PHPStan\Reflection\ReflectionProvider    resolvable
Pest\Rector\Contract\DocumentedRuleInterface  resolvable
composer audit                           clean

Two notes for review

phpstan/phpstan stays in require-dev. UsesToExtendRector constructor-injects PHPStan\Reflection\ReflectionProvider, which looks like the same class of bug, but it is not: rector/rector requires phpstan/phpstan: ^2.3.0 in its own require, so it arrives transitively in production. I checked the import list across src/ and config/ and that is the only other root namespace beyond Pest\Rector, PhpParser, PHPStan and Rector.

rector/rector: ^2.6.1 is already resolving to 2.7.0 in CI. Not changed here, but flagging it since CI and a local prefer-stable install can drift apart over time.

Out of scope

docs/rules.md was deleted in #10, so the repo now ships no rule documentation. getRuleDefinition() is implemented by all 61 rules and called by nothing. The data is still there, so a renderer plus a test that fails when the file drifts is a reasonable follow-up. Not holding this PR for it.

Refs #1940

`src/` imports 71 distinct `PhpParser\*` symbols across 465
references, but `nikic/php-parser` was never declared in `require`.
It only resolved because `phpunit/phpunit` allowed it, so the
version was never ours to control.

That mattered once #10 widened the `args` annotations to accept
`ArgPlaceholder`, a class added in php-parser 5.9.0. On
`prefer-lowest` composer resolves 5.8.0, where that class does not
exist, so `composer test:types` fails with 23 `class.notFound`
errors.

`static.yml` declared a `prefer-lowest` matrix entry but hardcoded
`composer update --prefer-stable` in the install step, so both legs
installed the same dependency set and PHPStan never saw the floor.
CI was green while the code was broken.

Adds a `composer audit --locked` step so an abandoned transitive
dependency fails CI, which is what the v5.0.5 change was for in the
first place.

Verified on both legs, at the corner CI resolves (rector 2.7.0,
phpstan 2.3.0):

  composer test:types  0 errors
  composer test:unit   290/290
  composer audit       clean

Refs #1940
@MrPunyapal
MrPunyapal merged commit 5febace into 5.x Oct 9, 2026
22 checks passed
@MrPunyapal
MrPunyapal deleted the fix/php-parser-floor branch October 9, 2026 11:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant