Skip to content

Resolve native type named resource to ResourceType instead of ObjectType in ParserNodeTypeToPHPStanType - #6378

Open
phpstan-bot wants to merge 5 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-nmx6jkb
Open

Resolve native type named resource to ResourceType instead of ObjectType in ParserNodeTypeToPHPStanType#6378
phpstan-bot wants to merge 5 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-nmx6jkb

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

On PHP 7.x, finfo_buffer($finfo, $content) was reported as Parameter #1 $finfo of function finfo_buffer expects resource, resource given. even though $finfo had been narrowed to resource.

The two resources in that message were not the same type: the expected one was an ObjectType for a class named resource, the given one a real ResourceType. This PR makes native types named resource resolve to ResourceType, which fixes a whole family of false positives on PHP 7.x signatures, not just finfo_buffer().

Changes

  • src/Type/ParserNodeTypeToPHPStanType.php: a Name node named resource (case-insensitive) now resolves to ResourceType instead of ObjectType('resource'). The check is placed before the self/static/parent handling so those keywords still resolve against the declaring class even if that class happens to be named resource.
  • tests/PHPStan/Rules/Functions/CallToFunctionParametersRulePhp7Test.php + data/bug-15141.php + data/call-to-function-php7.neon: new rule test pinned to phpVersion: 70400.
  • tests/PHPStan/Analyser/ResourceTypePhp7Test.php + data/bug-15141-php7.php: new type-inference test pinned to phpVersion: 70400.

Analogous cases fixed by the same change (all verified to fail before the fix):

  • Other parameters on the same axis: 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().
  • Return types, the mirror of the parameter case: pg_exec() and pg_loopen() returned ObjectType('resource')|false. Passing that to a function whose parameter really is resource (pg_fetch_row(), pg_num_rows(), pg_lo_read(), pg_lo_close(), or a userland @param resource) produced the same false positive, and is_resource() on it narrowed to *NEVER* with an "If condition is always false" error.
  • Native types merged into signature-map entries in FunctionSignatureMapProvider::createSignature() were ObjectType('resource') too, even for functions whose PHPDoc/map type was correct; they are now consistent.

Analogous cases probed and found to be already correct:

  • Every other type name used in #[LanguageLevelTypeAware] across phpstorm-stubs is a real class (CurlHandle, PgSql\Connection, finfo, ...) or a php-parser builtin; resource is the only pseudo-type, so there is no sibling mapping to add.
  • No internal class method, property or class constant declares a resource native type in the stubs today - but those all go through TypehintHelper::decideTypeFromReflection()ParserNodeTypeToPHPStanType::resolve(), so they are covered by the same fix.
  • Php8SignatureMapProvider and the signature map parser were already fine: they go through TypeNodeResolver, which maps resource to ResourceType.
  • PHP 8.0+ is unaffected: the stubs use real class types (finfo, CurlHandle, PgSql\Result, ...) there.

Root cause

ParserNodeTypeToPHPStanType::resolve() treats any native type expressed as a Name node as a class name. PhpStorm stubs describe pre-PHP 8 signatures with #[LanguageLevelTypeAware(['8.1' => 'finfo'], default: 'resource')], and better-reflection turns the resource string into a Name node because resource is not a php-parser builtin type. Every internal signature that used the pseudo-type resource therefore got ObjectType('resource') — a class type that describes itself as resource, is not a supertype of ResourceType, and makes is_resource() narrow to never.

Functions listed in resources/functionMap.php mostly hid the problem because the map's resource string goes through TypeNodeResolver, which correctly returns ResourceType; the bug only surfaced for functions missing from the map (finfo_buffer, finfo_file, curl_getinfo, most of the deprecated pg_* aliases) and for the native-type half of the mapped ones.

The fix mirrors what TypeNodeResolver already does for the resource PHPDoc type: an unqualified, global resource is the pseudo-type, never a class.

Test

  • CallToFunctionParametersRulePhp7Test::testBug15141 analyses tests/PHPStan/Rules/Functions/data/bug-15141.php (the reproducer from the issue plus the analogous parameter and return-type cases) at phpVersion: 70400 and expects no errors. Without the fix it reports 15 argument.type false positives.
  • ResourceTypePhp7Test asserts, at phpVersion: 70400, that finfo_open() infers resource for both the PHPDoc and the native type, that pg_exec() returns resource|false, and that is_resource() narrows pg_exec()/pg_loopen() results to resource. Without the fix the two is_resource() assertions produce *NEVER*.

Fixes phpstan/phpstan#15141

…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).
@staabm
staabm force-pushed the create-pull-request/patch-nmx6jkb branch from ca70b4e to ce66c8b Compare September 5, 2026 19:26
/**
* @extends RuleTestCase<CallToFunctionParametersRule>
*/
class CallToFunctionParametersRulePhp7Test extends RuleTestCase

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implement the test in CallToFunctionParametersRuleTest instead and use #[RequiresPhp('< 8.0.0')]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this test into tests/PHPStan/Analyser/nsrt and use <?php // lint < 8.0 to indicate the required php version

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.phptests/PHPStan/Analyser/nsrt/bug-15141-php7.php, first line changed to <?php // lint < 8.0; ResourceTypePhp7Test.php deleted. The file is now picked up by NodeScopeResolverTest and skipped on PHP ≥ 8.0 runtimes.
  • CallToFunctionParametersRulePhp7Test.php and data/call-to-function-php7.neon deleted; testBug15141() added to CallToFunctionParametersRuleTest with #[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: 70400 reading the same data files: 6/6 passing with the fix, and with src/Type/ParserNodeTypeToPHPStanType.php reverted to its pre-fix state, 3 failures (the two is_resource() assertions and the rule test's 15 argument.type false 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.

phpstan-bot and others added 2 commits September 5, 2026 19:42
…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>
@ondrejmirtes

Copy link
Copy Markdown
Member

Doesn't make sense, resource can' be a native typehint:

https://3v4l.org/h3QkW

@staabm staabm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a new e2e test to assert this PR does not change results of https://phpstan.org/r/47fe50ab-e9de-4e20-8093-b24400e53d66

phpstan-bot and others added 2 commits September 5, 2026 20:21
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>
@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

Done — pushed as 2df3eb94b and 627fb9b00.

What the review turned up

The playground link is not just a "no change expected" check — the PR did change its result. With the fix as it stood:

- test.php:13: Parameter #1 $r of function doFoo expects Resource, resource given.   (playground)
+ test.php:8:  Parameter #1 $r of function doFoo expects resource, Resource given.   (this PR)

The strtolower() comparison swallowed a userland class named Resource. That is also ondrejmirtes' objection: PHP has no resource native type — a resource typehint in real code is read as a class name (it only emits "resource" is not a supported builtin type and will be interpreted as a class name). The pseudo-type reaches ParserNodeTypeToPHPStanType only from the PhpStorm stubs, which spell it lowercase.

Changes

2df3eb94b — compare the name case-sensitively ($typeClassName === 'resource') instead of lowercasing first, with a comment explaining why. Plus testBug15141ResourceClass in CallToFunctionParametersRuleTest (runs on every PHP version, unlike the version-gated testBug15141): fails before this commit with the line-8/line-13 swap above, passes after.

627fb9b00e2e/bug-15141/ + a matrix entry in .github/workflows/e2e-tests.yml:

  • test.php is the playground snippet verbatim; asserted at the runtime PHP version and at phpVersion: 70400 to be exactly one error, test.php:13:...expects Resource, resource given. (the -a equals '1' "$(… grep -Ec …)" assertion is what catches the line-8 false positive and any extra error).
  • resource-pseudo-type.php is the issue's finfo_buffer() reproducer, analysed in the same phpVersion: 70400 run — which closes the gap flagged in the previous round, where moving the tests to #[RequiresPhp('< 8.0.0')] / // lint < 8.0 left the fix unexercised on PHP 8 CI runtimes. Without the fix that run reports 2 errors instead of 1. I ran the workflow script locally against a downloaded bashunit; it passes.

One thing worth knowing

My first version of the unit test data file used the playground's global doFoo()/doBar() names, which broke 42 unrelated tests — namespaced doFoo() calls in other data files fall back to the global one, so if (doFoo()) branches became unreachable. The functions are now prefixed (bug15141TakesResourceClass); the class stays global Resource, which is the part the regression needs. Related: a stale sys_get_temp_dir()/phpstan-tests cache keeps such failures alive across runs — worth rm -rfing before investigating a weird suite failure.

make tests (21274 tests, 98 skipped) and make phpstan are both green.

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.

3 participants