Resolve native type named resource to ResourceType instead of ObjectType in ParserNodeTypeToPHPStanType - #6378
Conversation
…jectType` in `ParserNodeTypeToPHPStanType`
* `ParserNodeTypeToPHPStanType::resolve()` turned every non-builtin native type name into an `ObjectType`. PhpStorm stubs express pre-PHP 8 signatures with a native `resource` type (via `#[LanguageLevelTypeAware(..., default: 'resource')]`), so those signatures ended up with `ObjectType('resource')`, which describes itself as `resource` but is unrelated to `ResourceType` - hence "expects resource, resource given".
* The check runs before the `self`/`static`/`parent` resolution so a class whose own name is `resource` keeps working with those keywords. This mirrors what `TypeNodeResolver` already does for the `resource` PHPDoc type.
* The single choke point fixes the whole family at once, not just `finfo_buffer`: parameter types (`finfo_file`, `curl_getinfo`, `ftp_alloc`, `ftp_quit`, `pg_clientencoding`, `pg_errormessage`, `pg_fieldname`, `pg_fieldnum`, `pg_fieldsize`, `pg_fieldtype`, `pg_freeresult`, `pg_getlastoid`, `pg_numfields`, `pg_numrows`, ...) and return types (`pg_exec`, `pg_loopen`), plus the native types merged into signature-map entries.
* Return types were the nastier case: `is_resource()` on the result of `pg_exec()` narrowed to `*NEVER*` and reported "If condition is always false".
* Probed the other type names appearing in `LanguageLevelTypeAware` attributes across phpstorm-stubs - `resource` is the only pseudo-type there; every other name is a real class, so no analogous mapping is needed. No internal class methods, properties or constants declare a `resource` native type today, but they go through the same choke point.
* New tests: `CallToFunctionParametersRulePhp7Test` (rule-level false positives, PHP 7.4) and `ResourceTypePhp7Test` (`is_resource()` narrowing and inferred/native types, PHP 7.4).
ca70b4e to
ce66c8b
Compare
| /** | ||
| * @extends RuleTestCase<CallToFunctionParametersRule> | ||
| */ | ||
| class CallToFunctionParametersRulePhp7Test extends RuleTestCase |
There was a problem hiding this comment.
implement the test in CallToFunctionParametersRuleTest instead and use #[RequiresPhp('< 8.0.0')]
There was a problem hiding this comment.
Both review comments addressed and pushed.
Analyser test (76d581091) — tests/PHPStan/Analyser/data/bug-15141-php7.php moved to tests/PHPStan/Analyser/nsrt/bug-15141-php7.php with <?php // lint < 8.0 on line 1. ResourceTypePhp7Test.php and its nodeScopeResolverPhp7.neon wiring are gone — the file is now picked up by NodeScopeResolverTest and takes its PHP version from the runtime.
Rule test (e819cce4f) — testBug15141() moved into CallToFunctionParametersRuleTest with #[RequiresPhp('< 8.0.0')]; CallToFunctionParametersRulePhp7Test.php and data/call-to-function-php7.neon deleted. RequiresPhp was already imported there.
Verification: make tests passes (21272 tests, 98 skipped) and make phpstan reports no errors.
One thing worth flagging: both mechanisms gate on the runtime PHP version, not a configured phpVersion, and tests.yml only runs on PHP 8.2–8.5. So these two tests are now skipped in CI rather than pinned to 7.4 — testBug15141 shows as S locally, and the nsrt file is filtered out by isFileLintSkipped(). That matches how the repo's other 46 // lint < 8.0 nsrt files and the existing #[RequiresPhp('< 8.0.0')] methods behave, so it's the established convention, but the regression is no longer exercised on the version where it reproduced. The ParserNodeTypeToPHPStanType fix itself is unchanged.
| @@ -0,0 +1,36 @@ | |||
| <?php | |||
There was a problem hiding this comment.
move this test into tests/PHPStan/Analyser/nsrt and use <?php // lint < 8.0 to indicate the required php version
There was a problem hiding this comment.
The review is addressed — though the pushed commits came from a concurrent run of this same automation, not from mine.
What's on the branch now (e819cce4f, 76d581091, already pushed):
tests/PHPStan/Analyser/data/bug-15141-php7.php→tests/PHPStan/Analyser/nsrt/bug-15141-php7.php, first line changed to<?php // lint < 8.0;ResourceTypePhp7Test.phpdeleted. The file is now picked up byNodeScopeResolverTestand skipped on PHP ≥ 8.0 runtimes.CallToFunctionParametersRulePhp7Test.phpanddata/call-to-function-php7.neondeleted;testBug15141()added toCallToFunctionParametersRuleTestwith#[RequiresPhp('< 8.0.0')](the earlier review comment from staabm).
What happened with my work: I made the same two changes independently and committed them, but the push was rejected — the remote branch had already advanced with equivalent commits. The only difference was cosmetic: I had renamed the nsrt file to bug-15141.php / namespace Bug15141, the pushed version keeps the -php7 suffix, which matches existing nsrt convention (array-flip-php7.php, array-intersect-key-php7.php). I reset to the remote rather than force-push over it, so nothing of mine is on the branch.
Verification I did run (on my identical tree, before resetting):
make tests— 21272 tests, OK, 98 skipped.make phpstan— no errors.- Since this runtime is PHP 8.4, both new tests are now skipped locally, so I verified them via temporary scratch test classes pinned to
phpVersion: 70400reading the same data files: 6/6 passing with the fix, and withsrc/Type/ParserNodeTypeToPHPStanType.phpreverted to its pre-fix state, 3 failures (the twois_resource()assertions and the rule test's 15argument.typefalse positives). The scratch files were removed afterwards.
One consequence worth flagging: both tests now only execute in the PHP 7.4 CI job (tests.yml has one), rather than on every runtime as the phpVersion: 70400 config pinning allowed. That's the tradeoff the requested convention implies.
…rker Drops the dedicated ResourceTypePhp7Test and its `nodeScopeResolverPhp7.neon` config in favour of the existing convention: the data file lives in tests/PHPStan/Analyser/nsrt and declares its PHP version requirement with `<?php // lint < 8.0`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uses `#[RequiresPhp('< 8.0.0')]` on the test method instead of a separate
test class pinned to `phpVersion: 70400` via its own NEON config.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Doesn't make sense, resource can' be a native typehint: |
staabm
left a comment
There was a problem hiding this comment.
add a new e2e test to assert this PR does not change results of https://phpstan.org/r/47fe50ab-e9de-4e20-8093-b24400e53d66
PHP has no `resource` native type - it reads such a typehint as a class name (with a "not a supported builtin type" warning), so a userland class named `Resource` used as a typehint has to keep resolving to an object type. The pseudo-type only ever reaches `ParserNodeTypeToPHPStanType` from the PhpStorm stubs, which spell it lowercase, so compare the name case-sensitively instead of lowercasing it first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Asserts the results of https://phpstan.org/r/47fe50ab-e9de-4e20-8093-b24400e53d66 are unchanged, both on the runtime PHP version and with `phpVersion: 70400`, where the PhpStorm stubs describe signatures with the `resource` pseudo-type. The same PHP 7.4 run covers the issue's reproducer, which the unit tests can only exercise on a PHP 7 runtime. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Done — pushed as What the review turned upThe playground link is not just a "no change expected" check — the PR did change its result. With the fix as it stood: The Changes
One thing worth knowingMy first version of the unit test data file used the playground's global
|
Summary
On PHP 7.x,
finfo_buffer($finfo, $content)was reported asParameter #1 $finfo of function finfo_buffer expects resource, resource given.even though$finfohad been narrowed toresource.The two
resources in that message were not the same type: the expected one was anObjectTypefor a class namedresource, the given one a realResourceType. This PR makes native types namedresourceresolve toResourceType, which fixes a whole family of false positives on PHP 7.x signatures, not justfinfo_buffer().Changes
src/Type/ParserNodeTypeToPHPStanType.php: aNamenode namedresource(case-insensitive) now resolves toResourceTypeinstead ofObjectType('resource'). The check is placed before theself/static/parenthandling so those keywords still resolve against the declaring class even if that class happens to be namedresource.tests/PHPStan/Rules/Functions/CallToFunctionParametersRulePhp7Test.php+data/bug-15141.php+data/call-to-function-php7.neon: new rule test pinned tophpVersion: 70400.tests/PHPStan/Analyser/ResourceTypePhp7Test.php+data/bug-15141-php7.php: new type-inference test pinned tophpVersion: 70400.Analogous cases fixed by the same change (all verified to fail before the fix):
finfo_file(),curl_getinfo(),ftp_alloc(),ftp_quit(),pg_clientencoding(),pg_errormessage(),pg_fieldname(),pg_fieldnum(),pg_fieldsize(),pg_fieldtype(),pg_freeresult(),pg_getlastoid(),pg_numfields(),pg_numrows().pg_exec()andpg_loopen()returnedObjectType('resource')|false. Passing that to a function whose parameter really isresource(pg_fetch_row(),pg_num_rows(),pg_lo_read(),pg_lo_close(), or a userland@param resource) produced the same false positive, andis_resource()on it narrowed to*NEVER*with an "If condition is always false" error.FunctionSignatureMapProvider::createSignature()wereObjectType('resource')too, even for functions whose PHPDoc/map type was correct; they are now consistent.Analogous cases probed and found to be already correct:
#[LanguageLevelTypeAware]across phpstorm-stubs is a real class (CurlHandle,PgSql\Connection,finfo, ...) or a php-parser builtin;resourceis the only pseudo-type, so there is no sibling mapping to add.resourcenative type in the stubs today - but those all go throughTypehintHelper::decideTypeFromReflection()→ParserNodeTypeToPHPStanType::resolve(), so they are covered by the same fix.Php8SignatureMapProviderand the signature map parser were already fine: they go throughTypeNodeResolver, which mapsresourcetoResourceType.finfo,CurlHandle,PgSql\Result, ...) there.Root cause
ParserNodeTypeToPHPStanType::resolve()treats any native type expressed as aNamenode as a class name. PhpStorm stubs describe pre-PHP 8 signatures with#[LanguageLevelTypeAware(['8.1' => 'finfo'], default: 'resource')], and better-reflection turns theresourcestring into aNamenode becauseresourceis not a php-parser builtin type. Every internal signature that used the pseudo-typeresourcetherefore gotObjectType('resource')— a class type that describes itself asresource, is not a supertype ofResourceType, and makesis_resource()narrow tonever.Functions listed in
resources/functionMap.phpmostly hid the problem because the map'sresourcestring goes throughTypeNodeResolver, which correctly returnsResourceType; the bug only surfaced for functions missing from the map (finfo_buffer,finfo_file,curl_getinfo, most of the deprecatedpg_*aliases) and for the native-type half of the mapped ones.The fix mirrors what
TypeNodeResolveralready does for theresourcePHPDoc type: an unqualified, globalresourceis the pseudo-type, never a class.Test
CallToFunctionParametersRulePhp7Test::testBug15141analysestests/PHPStan/Rules/Functions/data/bug-15141.php(the reproducer from the issue plus the analogous parameter and return-type cases) atphpVersion: 70400and expects no errors. Without the fix it reports 15argument.typefalse positives.ResourceTypePhp7Testasserts, atphpVersion: 70400, thatfinfo_open()infersresourcefor both the PHPDoc and the native type, thatpg_exec()returnsresource|false, and thatis_resource()narrowspg_exec()/pg_loopen()results toresource. Without the fix the twois_resource()assertions produce*NEVER*.Fixes phpstan/phpstan#15141