Detect analyser fixtures missing a sufficient // lint >= comment for the version-specific PHP syntax they use#5855
Conversation
…r the version-specific PHP syntax they use - Add `PHPStan\Build\RequiredPhpVersionVisitor`, a php-parser visitor that computes the minimum PHP version a file needs to be parsed based on the syntactic features it uses: enums, readonly properties/classes, pure intersection and DNF types, standalone `null`/`false`/`true` types, typed class constants, dynamic class constant fetch, first-class callable syntax, property hooks, asymmetric visibility and the pipe operator. - Add `RequiredPhpVersionCommentTest` which parses every `tests/PHPStan/Analyser/nsrt` fixture (those are excluded from parallel-lint and skipped via the `// lint` comment instead), and fails when a fixture uses syntax newer than the lower bound guaranteed by its `<?php // lint >= X.Y` comment. This catches fixtures that would produce a hard parse error on older PHP versions in CI. - Bring 20 existing `nsrt` fixtures into compliance by adding or bumping their `// lint >=` comments to match the syntax they actually use.
|
Oh, this is so nice. |
| */ | ||
| public static function dataFixtures(): iterable | ||
| { | ||
| $directory = __DIR__ . '/../Analyser/nsrt'; |
There was a problem hiding this comment.
I think we can extend this directory list and add all data named folders below tests/PHPStan (no matter in which level of nesting)
There was a problem hiding this comment.
I stand corrected. only below tests/PHPStan/Rules
There was a problem hiding this comment.
Done. Here's a summary of how I addressed staabm's review.
What the review asked
extend this directory list to add all
datanamed folders belowtests/PHPStan(no matter in which level of nesting)
What I did
1. Extended the fixture coverage (RequiredPhpVersionCommentTest.php)
dataFixtures() now scans the nsrt directory plus every data directory anywhere below tests/PHPStan (54 data dirs, ~4900 fixtures total). While wiring this up I had to account for how data fixtures differ from nsrt:
- Comment placement —
nsrtfixtures are runtime-skipped byTypeInferenceTestCase::isFileLintSkipped, which requires the// lintcomment immediately after<?php.datafixtures aren't skipped that way (they're excluded fromparallel-lintas whole dirs/files and gated bymarkTestSkippedin the rule tests), and many keep their comment afterdeclare(...)on line 1. The detection now matches the comment anywhere on the first line, so I could bring fixtures into compliance by appending to line 1 — no line numbers shift, keeping the rule tests' line-based expectations valid. - Parse-error fixtures — some
datafixtures intentionally contain syntax errors (e.g.parse-error.php,abstract-final-property-hook-parse-error.php); these are now skipped instead of failing. - Printer round-trip snapshots —
cleaning-property-hooks-before/after.phpare asserted verbatim asCleaningParserTest's pretty-printer output, so a first-line comment can't be expressed in them. They're parsed by nikic/php-parser regardless of host PHP and excluded fromparallel-lint, so they're explicitly excluded via a documented constant.
2. Brought 57 data fixtures into compliance — added/bumped the // lint >= X.Y comment (appended to line 1) on every data fixture that used version-specific syntax without a sufficient declared minimum (enums/readonly/first-class-callable → 8.1, readonly classes/standalone types → 8.2, typed/dynamic class constants → 8.3, property hooks/asymmetric visibility → 8.4, #[NoDiscard]/pipe → 8.5).
Verification
- The extended test reported 59 failures before the fixture fixes (reproducing the gap the reviewer pointed at), green after.
make phpstan— No errorsmake tests— 17561 tests OK (96 standard version-gated skips)make cs— clean
Two commits pushed (test change, then fixture compliance).
There was a problem hiding this comment.
Both green. Here's a summary of what I did.
Summary
The review asked to scope the fixture scan in RequiredPhpVersionCommentTest to only data directories below tests/PHPStan/Rules.
Context I uncovered
When I started, the remote branch had already moved past the original PR diff: two commits (214a6eedf, 443257a18) had implemented the reviewer's first suggestion — scanning every data directory below tests/PHPStan. The current review ("I stand corrected. only below tests/PHPStan/Rules") is the correction to that, so the task was to narrow the scan.
My local exploratory commit conflicted with that newer remote work, so I reset to the remote HEAD and applied the narrowing on top of the existing infrastructure.
Change made (commit 095a8811d)
In tests/PHPStan/Build/RequiredPhpVersionCommentTest.php:
- Changed the data-directory finder from
->in(__DIR__ . '/..')(all oftests/PHPStan) to->in(__DIR__ . '/../Rules'). - Removed the now-unneeded
EXCLUDED_FIXTURESallowlist and itsin_arrayfilter — the only entries (cleaning-property-hooks-*) live underParser/data, which is no longer scanned. - Updated the docblock to say "every
datadirectory below tests/PHPStan/Rules".
The lenient first-line // lint matching the remote introduced stays — I confirmed why it's correct: SkipLintProcess runs the skip script via php -r after str_replace('<?php', '', $script), which strips <?php from inside the matching regex too, so parallel-lint honours a // lint comment anywhere on the first line (e.g. after declare(...)), while TypeInferenceTestCase keeps the strict <?php-anchored placement for nsrt.
Verification
RequiredPhpVersionCommentTestgreen (4376 assertions).- Confirmed it still catches a real gap by temporarily dropping an un-commented
enumfixture intoRules/Comparison/data(failed with the actionable message), then removed it. make testsgreen (17045 tests, 96 pre-existing skips).make phpstangreen.
Pushed to create-pull-request/patch-ml2b3e8.
One note for the reviewer: the earlier remote commit 443257a18 added // lint comments to some non-Rules data fixtures (e.g. under Parser/data) that are no longer scanned after this narrowing. I left them in place since they're harmless and valid — let me know if you'd prefer those reverted.
Extend the directory list beyond `nsrt` to every `data` directory anywhere below tests/PHPStan, so excluded fixtures across the rule/parser/analyser tests are verified as well. The comment is matched anywhere on the first line (these fixtures are excluded from parallel-lint, and `data` fixtures keep the comment after `declare(...)` to avoid shifting line-sensitive assertions), intentional parse-error fixtures are skipped, and printer round-trip snapshots are excluded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add or bump the first-line `// lint >= X.Y` comment on the 57 `data` fixtures that use version-specific PHP syntax without advertising a sufficient minimum version. The comment is appended to the existing first line so no line numbers shift and the rule tests' line-based expectations stay valid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Narrow the lint-comment check from every data directory under tests/PHPStan to just those below tests/PHPStan/Rules, as requested in review. The cleaning-property-hooks snapshot fixtures live under Parser/data and are no longer scanned, so the EXCLUDED_FIXTURES allowlist is no longer needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
It's unclear to me why // lint >= was added on so much file given the fact test were already working (?) |
|
most files changed (or whole folders) were on the consider the changed files a "baseline cleanup", so for the future the newly added test only needs to deal with newly added files (and we don't need to list all the excludes additionally somewhere else). the PR is more about what this enables in the future. see motivation described in phpstan/phpstan#14816 |
Should we remove some ? |
we can't remove them because the files contain e.g. syntax/linting errors, see e.g. #5856 |
|
Should we also check for |
staabm
left a comment
There was a problem hiding this comment.
add support for a mixed native type which requires PHP 8.0+
A native `mixed` type hint parses as a class name on PHP < 8.0 (php-parser yields a Node\Name there) and only becomes the builtin mixed type in 8.0+, so PHPStan analyses such fixtures differently across versions. Flag it so fixtures using it advertise `// lint >= 8.0`. Files with an upper-bound `// lint` constraint deliberately target old PHP (e.g. to assert a version-specific function signature); since `mixed` is not a parse error there, they are exempt from the requirement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brings existing fixtures into compliance with the new mixed-type detection so they are skipped on PHP versions where `mixed` is analysed as a class type. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
All done. Here's a summary of how I addressed staabm's review. Review
What I implemented1. Detection in the visitor ( 2. Unit tests ( Key finding that justifies the check
Handling the upper-bound edge caseTwo fixtures ( Fixture complianceBrought 95 fixtures into compliance by adding Verification
Two commits pushed to |
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
All green (17370 tests, 96 standard version-gated skips). SummaryI addressed staabm's review: "track and report the line of the node which is the reason for the minimum required php version." Changes (commit
|
Summary
New test fixtures regularly slip into the repository using PHP syntax that is newer than the minimum PHP version they declare (or without declaring one at all). Because local development happens on modern PHP, these only blow up later in CI on PHP 7.4/8.0 as hard parse errors (see e.g. #5850).
tests/PHPStan/Analyser/nsrtfixtures are excluded fromparallel-lint(Makefile) and instead rely on a first-line<?php // lint >= X.Ycomment to be skipped on older PHP versions. Nothing verified that this comment was actually present and high enough for the syntax used. This change adds that verification and brings the existing fixtures into compliance.As suggested in the issue, the detection runs as a
TypeInferenceTestCase-style test rather than a PHPStan rule, since fixture files are excluded from PHPStan's own analysis.Changes
build/PHPStan/Build/RequiredPhpVersionVisitor.php— aphp-parservisitor that walks a fixture's AST and reports the minimum PHP version required to parse it, together with a human-readable reason. Detected features:null/false/truetypes, disjunctive normal form types → 8.2tests/PHPStan/Build/RequiredPhpVersionCommentTest.php:testFixtureHasRequiredLintCommentparses everynsrtfixture and asserts the version guaranteed by its// lintcomment (only lower-bound>=/>/==constraints guarantee newer syntax) covers the version the syntax requires, with an actionable failure message.testDetectedVersionunit-tests the visitor against a snippet per feature.nsrtfixtures into compliance by adding/bumping their// lint >=comment: array-key-exists-on-subtracted, array-map, array_values, bug-10037, bug-12691, bug-13061, bug-13828, bug-13872, bug-14772, bug-14774, bug-4890-php8, bug-6904, bug-7698, bug-7944, bug-nullsafe-prop-static-access, class-constant-narrowing, discussion-13395, ds-copy, override-property-union-type-with-tag-subtype, static-late-binding.Root cause
The
nsrtfixtures are excluded fromparallel-lintand depend solely on the// lintcomment to be skipped on PHP versions that cannot parse them. The convention was applied by hand and unenforced, so a fixture could use e.g. anenum(PHP 8.1) while declaring// lint >= 8.0or no comment at all. On a modern host that parses and analyses fine, hiding the latent CI parse error. The new test enforces the convention mechanically across the whole directory.Test
RequiredPhpVersionCommentTest::testFixtureHasRequiredLintCommentreproduced the bug: before the fixture fixes it reported 20 failures (e.g. "bug-14774.php uses enums which requires PHP 8.1. Add a<?php // lint >= 8.1comment …"). After bumping the comments it is green.RequiredPhpVersionCommentTest::testDetectedVersioncovers each detected feature (enum, readonly property/promoted/class, intersection/DNF types, standalonenull/false/truetypes, typed class constant, dynamic class constant fetch, first-class callable, property hook, asymmetric visibility, pipe operator) plus a plain-code negative case.make phpstanpasses for the new sources; the affectedNodeScopeResolverTestand rule tests (CallMethodsRuleTest,MatchExpressionRuleTest) referencing the touched fixtures stay green.Fixes phpstan/phpstan#14816