From ce66c8bd0f0d0b5f823ee2ddd23dac18b03ddb95 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sat, 5 Sep 2026 13:13:01 +0000 Subject: [PATCH 1/9] Resolve native type named `resource` to `ResourceType` instead of `ObjectType` 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). --- src/Type/ParserNodeTypeToPHPStanType.php | 5 + .../PHPStan/Analyser/ResourceTypePhp7Test.php | 36 ++++++++ .../PHPStan/Analyser/data/bug-15141-php7.php | 36 ++++++++ .../CallToFunctionParametersRulePhp7Test.php | 59 ++++++++++++ .../Rules/Functions/data/bug-15141.php | 91 +++++++++++++++++++ .../Functions/data/call-to-function-php7.neon | 2 + 6 files changed, 229 insertions(+) create mode 100644 tests/PHPStan/Analyser/ResourceTypePhp7Test.php create mode 100644 tests/PHPStan/Analyser/data/bug-15141-php7.php create mode 100644 tests/PHPStan/Rules/Functions/CallToFunctionParametersRulePhp7Test.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-15141.php create mode 100644 tests/PHPStan/Rules/Functions/data/call-to-function-php7.neon diff --git a/src/Type/ParserNodeTypeToPHPStanType.php b/src/Type/ParserNodeTypeToPHPStanType.php index e616fffc0ed..c77aa37b17d 100644 --- a/src/Type/ParserNodeTypeToPHPStanType.php +++ b/src/Type/ParserNodeTypeToPHPStanType.php @@ -26,6 +26,11 @@ public static function resolve($type, ?ClassReflection $classReflection): Type } elseif ($type instanceof Name) { $typeClassName = (string) $type; $lowercasedClassName = strtolower($typeClassName); + if ($lowercasedClassName === 'resource') { + // PhpStorm stubs describe pre-PHP 8 signatures with a `resource` native type + return new ResourceType(); + } + if ($classReflection !== null && in_array($lowercasedClassName, ['self', 'static'], true)) { if ($lowercasedClassName === 'static') { return new StaticType($classReflection); diff --git a/tests/PHPStan/Analyser/ResourceTypePhp7Test.php b/tests/PHPStan/Analyser/ResourceTypePhp7Test.php new file mode 100644 index 00000000000..c2d372d9a6a --- /dev/null +++ b/tests/PHPStan/Analyser/ResourceTypePhp7Test.php @@ -0,0 +1,36 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/nodeScopeResolverPhp7.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-15141-php7.php b/tests/PHPStan/Analyser/data/bug-15141-php7.php new file mode 100644 index 00000000000..fe44fb53d4e --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-15141-php7.php @@ -0,0 +1,36 @@ + + */ +class CallToFunctionParametersRulePhp7Test extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = self::createReflectionProvider(); + return new CallToFunctionParametersRule( + $broker, + new FunctionCallParametersCheck( + new RuleLevelHelper( + $broker, + checkNullables: true, + checkThisOnly: false, + checkUnionTypes: true, + checkExplicitMixed: true, + checkImplicitMixed: true, + checkBenevolentUnionTypes: false, + discoveringSymbolsTip: true, + ), + new NullsafeCheck(), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), + $broker, + checkArgumentTypes: true, + checkArgumentsPassedByReference: true, + checkExtraArguments: true, + checkMissingTypehints: true, + ), + ); + } + + public function testBug15141(): void + { + $this->analyse([__DIR__ . '/data/bug-15141.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/data/call-to-function-php7.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-15141.php b/tests/PHPStan/Rules/Functions/data/bug-15141.php new file mode 100644 index 00000000000..feb2acde312 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-15141.php @@ -0,0 +1,91 @@ + Date: Sat, 5 Sep 2026 19:42:30 +0000 Subject: [PATCH 2/9] Move resource type-inference test into nsrt with a `// lint < 8.0` marker 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 ` --- .../PHPStan/Analyser/ResourceTypePhp7Test.php | 36 ------------------- .../{data => nsrt}/bug-15141-php7.php | 2 +- 2 files changed, 1 insertion(+), 37 deletions(-) delete mode 100644 tests/PHPStan/Analyser/ResourceTypePhp7Test.php rename tests/PHPStan/Analyser/{data => nsrt}/bug-15141-php7.php (97%) diff --git a/tests/PHPStan/Analyser/ResourceTypePhp7Test.php b/tests/PHPStan/Analyser/ResourceTypePhp7Test.php deleted file mode 100644 index c2d372d9a6a..00000000000 --- a/tests/PHPStan/Analyser/ResourceTypePhp7Test.php +++ /dev/null @@ -1,36 +0,0 @@ -assertFileAsserts($assertType, $file, ...$args); - } - - public static function getAdditionalConfigFiles(): array - { - return [ - __DIR__ . '/nodeScopeResolverPhp7.neon', - ]; - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-15141-php7.php b/tests/PHPStan/Analyser/nsrt/bug-15141-php7.php similarity index 97% rename from tests/PHPStan/Analyser/data/bug-15141-php7.php rename to tests/PHPStan/Analyser/nsrt/bug-15141-php7.php index fe44fb53d4e..3fac762e78f 100644 --- a/tests/PHPStan/Analyser/data/bug-15141-php7.php +++ b/tests/PHPStan/Analyser/nsrt/bug-15141-php7.php @@ -1,4 +1,4 @@ - Date: Sat, 5 Sep 2026 19:42:30 +0000 Subject: [PATCH 3/9] Move resource parameter test into CallToFunctionParametersRuleTest 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 --- .../CallToFunctionParametersRulePhp7Test.php | 59 ------------------- .../CallToFunctionParametersRuleTest.php | 6 ++ .../Functions/data/call-to-function-php7.neon | 2 - 3 files changed, 6 insertions(+), 61 deletions(-) delete mode 100644 tests/PHPStan/Rules/Functions/CallToFunctionParametersRulePhp7Test.php delete mode 100644 tests/PHPStan/Rules/Functions/data/call-to-function-php7.neon diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRulePhp7Test.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRulePhp7Test.php deleted file mode 100644 index 1a003dbd0f1..00000000000 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRulePhp7Test.php +++ /dev/null @@ -1,59 +0,0 @@ - - */ -class CallToFunctionParametersRulePhp7Test extends RuleTestCase -{ - - protected function getRule(): Rule - { - $broker = self::createReflectionProvider(); - return new CallToFunctionParametersRule( - $broker, - new FunctionCallParametersCheck( - new RuleLevelHelper( - $broker, - checkNullables: true, - checkThisOnly: false, - checkUnionTypes: true, - checkExplicitMixed: true, - checkImplicitMixed: true, - checkBenevolentUnionTypes: false, - discoveringSymbolsTip: true, - ), - new NullsafeCheck(), - new UnresolvableTypeHelper(), - new PropertyReflectionFinder(), - $broker, - checkArgumentTypes: true, - checkArgumentsPassedByReference: true, - checkExtraArguments: true, - checkMissingTypehints: true, - ), - ); - } - - public function testBug15141(): void - { - $this->analyse([__DIR__ . '/data/bug-15141.php'], []); - } - - public static function getAdditionalConfigFiles(): array - { - return [ - __DIR__ . '/data/call-to-function-php7.neon', - ]; - } - -} diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 22c4c24fa8f..da628410882 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -3109,4 +3109,10 @@ public function testBug15168(): void $this->analyse([__DIR__ . '/data/bug-15168.php'], []); } + #[RequiresPhp('< 8.0.0')] + public function testBug15141(): void + { + $this->analyse([__DIR__ . '/data/bug-15141.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/call-to-function-php7.neon b/tests/PHPStan/Rules/Functions/data/call-to-function-php7.neon deleted file mode 100644 index a9d8e60640e..00000000000 --- a/tests/PHPStan/Rules/Functions/data/call-to-function-php7.neon +++ /dev/null @@ -1,2 +0,0 @@ -parameters: - phpVersion: 70400 From 2df3eb94ba2ce2ec402ad4116c1ce385ab3bb703 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 5 Sep 2026 20:21:13 +0000 Subject: [PATCH 4/9] Resolve only a lowercase `resource` native type to `ResourceType` 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 --- src/Type/ParserNodeTypeToPHPStanType.php | 7 +++++-- .../CallToFunctionParametersRuleTest.php | 10 ++++++++++ .../data/bug-15141-resource-class.php | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-15141-resource-class.php diff --git a/src/Type/ParserNodeTypeToPHPStanType.php b/src/Type/ParserNodeTypeToPHPStanType.php index c77aa37b17d..87f645cdb2a 100644 --- a/src/Type/ParserNodeTypeToPHPStanType.php +++ b/src/Type/ParserNodeTypeToPHPStanType.php @@ -26,8 +26,11 @@ public static function resolve($type, ?ClassReflection $classReflection): Type } elseif ($type instanceof Name) { $typeClassName = (string) $type; $lowercasedClassName = strtolower($typeClassName); - if ($lowercasedClassName === 'resource') { - // PhpStorm stubs describe pre-PHP 8 signatures with a `resource` native type + if ($typeClassName === 'resource') { + // PhpStorm stubs describe pre-PHP 8 signatures with a `resource` native type. + // Compared case-sensitively on purpose: PHP has no `resource` native type, + // it reads such a typehint as a class name, so a userland class named + // `Resource` has to keep resolving to an object type. return new ResourceType(); } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index da628410882..9d199187722 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -3115,4 +3115,14 @@ public function testBug15141(): void $this->analyse([__DIR__ . '/data/bug-15141.php'], []); } + public function testBug15141ResourceClass(): void + { + $this->analyse([__DIR__ . '/data/bug-15141-resource-class.php'], [ + [ + 'Parameter #1 $r of function bug15141TakesResourceClass expects Resource, resource given.', + 16, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-15141-resource-class.php b/tests/PHPStan/Rules/Functions/data/bug-15141-resource-class.php new file mode 100644 index 00000000000..cb097edf6e2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-15141-resource-class.php @@ -0,0 +1,18 @@ + Date: Sat, 5 Sep 2026 20:21:13 +0000 Subject: [PATCH 5/9] Add e2e test for a userland class named `Resource` 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 --- .github/workflows/e2e-tests.yml | 14 ++++++++++++++ e2e/bug-15141/php74.neon | 6 ++++++ e2e/bug-15141/phpstan.neon | 4 ++++ e2e/bug-15141/resource-pseudo-type.php | 9 +++++++++ e2e/bug-15141/test.php | 15 +++++++++++++++ 5 files changed, 48 insertions(+) create mode 100644 e2e/bug-15141/php74.neon create mode 100644 e2e/bug-15141/phpstan.neon create mode 100644 e2e/bug-15141/resource-pseudo-type.php create mode 100644 e2e/bug-15141/test.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index ae29efb84c7..c69d2c72126 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -1407,6 +1407,20 @@ jobs: - script: | cd e2e/bug-14305 ../../bin/phpstan + - script: | + # A userland class named `Resource` is a class, not the `resource` pseudo-type, + # on every phpVersion: https://phpstan.org/r/47fe50ab-e9de-4e20-8093-b24400e53d66 + # The `resource` native type in the pre-PHP 8 PhpStorm stubs is the pseudo-type, + # which is what https://github.com/phpstan/phpstan/issues/15141 was about. + cd e2e/bug-15141 + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse --no-progress --error-format raw") + echo "$OUTPUT" + ../bashunit -a contains 'test.php:13:Parameter #1 $r of function doFoo expects Resource, resource given.' "$OUTPUT" + ../bashunit -a equals '1' "$(echo "$OUTPUT" | grep -Ec '^/.+:[0-9]+:')" + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse --no-progress --error-format raw -c php74.neon") + echo "$OUTPUT" + ../bashunit -a contains 'test.php:13:Parameter #1 $r of function doFoo expects Resource, resource given.' "$OUTPUT" + ../bashunit -a equals '1' "$(echo "$OUTPUT" | grep -Ec '^/.+:[0-9]+:')" - script: | cd e2e/composer-and-phpstan-version-config composer install --ignore-platform-reqs diff --git a/e2e/bug-15141/php74.neon b/e2e/bug-15141/php74.neon new file mode 100644 index 00000000000..0501c78696d --- /dev/null +++ b/e2e/bug-15141/php74.neon @@ -0,0 +1,6 @@ +parameters: + level: 10 + phpVersion: 70400 + paths: + - test.php + - resource-pseudo-type.php diff --git a/e2e/bug-15141/phpstan.neon b/e2e/bug-15141/phpstan.neon new file mode 100644 index 00000000000..55b68fdc257 --- /dev/null +++ b/e2e/bug-15141/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 10 + paths: + - test.php diff --git a/e2e/bug-15141/resource-pseudo-type.php b/e2e/bug-15141/resource-pseudo-type.php new file mode 100644 index 00000000000..c81627cbb37 --- /dev/null +++ b/e2e/bug-15141/resource-pseudo-type.php @@ -0,0 +1,9 @@ + Date: Sun, 6 Sep 2026 08:38:25 +0000 Subject: [PATCH 6/9] Remove unit test declaring a global class named `Resource` Declaring a top-level class in a rule test data file pollutes the class namespace shared by the rest of the test suite. The e2e test in `e2e/bug-15141` already covers the userland `Resource` class case. Co-Authored-By: Claude Opus 5 --- .../CallToFunctionParametersRuleTest.php | 10 ---------- .../data/bug-15141-resource-class.php | 18 ------------------ 2 files changed, 28 deletions(-) delete mode 100644 tests/PHPStan/Rules/Functions/data/bug-15141-resource-class.php diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 9d199187722..da628410882 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -3115,14 +3115,4 @@ public function testBug15141(): void $this->analyse([__DIR__ . '/data/bug-15141.php'], []); } - public function testBug15141ResourceClass(): void - { - $this->analyse([__DIR__ . '/data/bug-15141-resource-class.php'], [ - [ - 'Parameter #1 $r of function bug15141TakesResourceClass expects Resource, resource given.', - 16, - ], - ]); - } - } diff --git a/tests/PHPStan/Rules/Functions/data/bug-15141-resource-class.php b/tests/PHPStan/Rules/Functions/data/bug-15141-resource-class.php deleted file mode 100644 index cb097edf6e2..00000000000 --- a/tests/PHPStan/Rules/Functions/data/bug-15141-resource-class.php +++ /dev/null @@ -1,18 +0,0 @@ - Date: Sun, 6 Sep 2026 10:43:52 +0200 Subject: [PATCH 7/9] Delete tests/PHPStan/Analyser/nsrt/bug-15141-php7.php --- .../PHPStan/Analyser/nsrt/bug-15141-php7.php | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 tests/PHPStan/Analyser/nsrt/bug-15141-php7.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-15141-php7.php b/tests/PHPStan/Analyser/nsrt/bug-15141-php7.php deleted file mode 100644 index 3fac762e78f..00000000000 --- a/tests/PHPStan/Analyser/nsrt/bug-15141-php7.php +++ /dev/null @@ -1,36 +0,0 @@ - Date: Sun, 6 Sep 2026 10:46:15 +0200 Subject: [PATCH 8/9] verify tests --- src/Type/ParserNodeTypeToPHPStanType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/ParserNodeTypeToPHPStanType.php b/src/Type/ParserNodeTypeToPHPStanType.php index 87f645cdb2a..ec10edde7c7 100644 --- a/src/Type/ParserNodeTypeToPHPStanType.php +++ b/src/Type/ParserNodeTypeToPHPStanType.php @@ -31,7 +31,7 @@ public static function resolve($type, ?ClassReflection $classReflection): Type // Compared case-sensitively on purpose: PHP has no `resource` native type, // it reads such a typehint as a class name, so a userland class named // `Resource` has to keep resolving to an object type. - return new ResourceType(); + // return new ResourceType(); } if ($classReflection !== null && in_array($lowercasedClassName, ['self', 'static'], true)) { From 3b26a978062141b07a8157cbf4d65dd4c2a0b966 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 6 Sep 2026 10:52:05 +0200 Subject: [PATCH 9/9] simplify tests --- .../CallToFunctionParametersRuleTest.php | 1 - .../Rules/Functions/data/bug-15141.php | 55 ------------------- 2 files changed, 56 deletions(-) diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index da628410882..7ff81f04225 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -3109,7 +3109,6 @@ public function testBug15168(): void $this->analyse([__DIR__ . '/data/bug-15168.php'], []); } - #[RequiresPhp('< 8.0.0')] public function testBug15141(): void { $this->analyse([__DIR__ . '/data/bug-15141.php'], []); diff --git a/tests/PHPStan/Rules/Functions/data/bug-15141.php b/tests/PHPStan/Rules/Functions/data/bug-15141.php index feb2acde312..e46d47ed525 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-15141.php +++ b/tests/PHPStan/Rules/Functions/data/bug-15141.php @@ -34,58 +34,3 @@ function doBaz(): void ftp_alloc($ftp, 1); ftp_quit($ftp); } - -function doLorem(): void -{ - $connection = pg_connect(''); - if ($connection === false) { - throw new \RuntimeException(''); - } - - pg_clientencoding($connection); - pg_errormessage($connection); - - $result = pg_query($connection, 'SELECT 1'); - if ($result === false) { - throw new \RuntimeException(''); - } - - pg_fieldname($result, 1); - pg_fieldnum($result, 'foo'); - pg_fieldsize($result, 1); - pg_fieldtype($result, 1); - pg_getlastoid($result); - pg_numfields($result); - pg_numrows($result); - pg_freeresult($result); -} - -/** @param resource $r */ -function takesResource($r): void -{ -} - -function doIpsum(): void -{ - $connection = pg_connect(''); - if ($connection === false) { - throw new \RuntimeException(''); - } - - $result = pg_exec($connection, 'SELECT 1'); - if ($result === false) { - throw new \RuntimeException(''); - } - - takesResource($result); - pg_fetch_row($result); - pg_num_rows($result); - - $lob = pg_loopen($connection, 1, 'r'); - if ($lob === false) { - throw new \RuntimeException(''); - } - - pg_lo_read($lob, 1); - pg_lo_close($lob); -}