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 01/10] 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 02/10] 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 03/10] 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 04/10] 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 05/10] 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 06/10] 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 07/10] 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:52:05 +0200 Subject: [PATCH 08/10] 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); -} From 09966d1e8b4a658706a87367b7767a3719a8325d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 6 Sep 2026 13:15:16 +0200 Subject: [PATCH 09/10] patch PhpStormStubsSourceStubber --- composer.json | 3 +++ composer.lock | 2 +- patches/PhpStormStubsSourceStubber.patch | 17 +++++++++++++++++ src/Type/ParserNodeTypeToPHPStanType.php | 8 -------- 4 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 patches/PhpStormStubsSourceStubber.patch diff --git a/composer.json b/composer.json index 8d8f339a09d..1bb556cea52 100644 --- a/composer.json +++ b/composer.json @@ -141,6 +141,9 @@ "nette/di": [ "patches/Resolver.patch" ], + "ondrejmirtes/better-reflection": [ + "patches/PhpStormStubsSourceStubber.patch" + ], "symfony/console": [ "patches/OutputFormatter.patch", "patches/Application.patch" diff --git a/composer.lock b/composer.lock index 7165ad9d85e..1c82a5a30f8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "55d62e3e19cd6606c2f5269dd2b68db1", + "content-hash": "192cb5806aaaedb9db917f5473cda4bd", "packages": [ { "name": "clue/ndjson-react", diff --git a/patches/PhpStormStubsSourceStubber.patch b/patches/PhpStormStubsSourceStubber.patch new file mode 100644 index 00000000000..d53b8eeacbf --- /dev/null +++ b/patches/PhpStormStubsSourceStubber.patch @@ -0,0 +1,17 @@ +--- a/src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php ++++ b/src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php +@@ -991,6 +991,14 @@ + return null; + } + ++ // `resource` is a pseudo-type, PHP has no such native type - a `resource` ++ // typehint is read back as a class name. Stubs use it in ++ // `#[LanguageLevelTypeAware]` to describe pre-PHP 8 signatures, so drop the ++ // native type there and let the PHPDoc in the same stub describe the type. ++ if (in_array('resource', explode('|', ltrim($type, '?')), true)) { ++ return null; ++ } ++ + /** @psalm-suppress InternalClass, InternalMethod */ + return BuilderHelpers::normalizeType($type); + } diff --git a/src/Type/ParserNodeTypeToPHPStanType.php b/src/Type/ParserNodeTypeToPHPStanType.php index 87f645cdb2a..e616fffc0ed 100644 --- a/src/Type/ParserNodeTypeToPHPStanType.php +++ b/src/Type/ParserNodeTypeToPHPStanType.php @@ -26,14 +26,6 @@ public static function resolve($type, ?ClassReflection $classReflection): Type } elseif ($type instanceof Name) { $typeClassName = (string) $type; $lowercasedClassName = strtolower($typeClassName); - 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(); - } - if ($classReflection !== null && in_array($lowercasedClassName, ['self', 'static'], true)) { if ($lowercasedClassName === 'static') { return new StaticType($classReflection); From f77672097fea3550e2a7e77284c06b7001adb532 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 6 Sep 2026 12:01:17 +0000 Subject: [PATCH 10/10] Move the `resource` pseudo-type into the PHPDoc instead of dropping it The previous version of the patch returned `null` from `normalizeType()` for a `resource` type and relied on the stub's own PHPDoc to describe the parameter or return value. That premise only holds for fileinfo and ftp. Out of the 30 affected functions that are not in `resources/functionMap.php`, only 4 carry a typed `@param resource`, so dropping the native type silently dropped the parameter check with it: pg_numrows('not a resource') // `@param $result`, so `mixed` - no error pg_freeresult(42) // `@param $result`, so `mixed` - no error `pg_loopen()` was worse: it is natively `resource|false` but its docblock says just `@return resource`, so dropping the native type traded one false positive for another - `$lob === false` became "will always evaluate to false". Rather than dropping the type, write it into the PHPDoc as `@param resource $x` / `@return resource|false`. `PhpDocNodeResolver` resolves same-named tags last-wins, so the synthesized tag overrides the stub's untyped or incomplete one, and `TypeNodeResolver` maps `resource` to `ResourceType` - which is the whole point of the fix. All 217 stub functions using the pseudo-type now infer exactly the signature they inferred before this PR at phpVersion 70400, 80000 and 80100. The only thing that changes is that the `resource` in them is a real `ResourceType` instead of `ObjectType('resource')`, so it is no longer a class that describes itself as `resource`, is not a supertype of `ResourceType`, and narrows to `never` under `is_resource()`. `addAnnotationToDocComment()` is reimplemented on top of the new `addTagToDocComment()`, which takes a whole tag rather than a bare name and builds the replacement in a callback so `$` in the tag text is not read as a backreference. The e2e gains `resource-pseudo-type-errors.php`, pinning the parameter checks that must survive, and `resource-pseudo-type.php` gains the `pg_loopen()` and `pg_exec()` cases. The three vendor states produce three different error sets, so the assertions catch both the original bug and these two regressions. --- .github/workflows/e2e-tests.yml | 7 +- e2e/bug-15141/php74.neon | 1 + e2e/bug-15141/resource-pseudo-type-errors.php | 7 + e2e/bug-15141/resource-pseudo-type.php | 24 ++++ patches/PhpStormStubsSourceStubber.patch | 132 +++++++++++++++++- 5 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 e2e/bug-15141/resource-pseudo-type-errors.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index c69d2c72126..73339f8b5e0 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -1420,7 +1420,12 @@ jobs: 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]+:')" + # The pseudo-type still has to check parameters, even where the stub + # has no `@param` type of its own to fall back on. + ../bashunit -a contains 'resource-pseudo-type-errors.php:5:Parameter #1 $result of function pg_numrows expects resource, string given.' "$OUTPUT" + ../bashunit -a contains 'resource-pseudo-type-errors.php:6:Parameter #1 $result of function pg_freeresult expects resource, int given.' "$OUTPUT" + ../bashunit -a contains 'resource-pseudo-type-errors.php:7:Parameter #1 $finfo of function finfo_buffer expects resource, int given.' "$OUTPUT" + ../bashunit -a equals '4' "$(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 index 0501c78696d..ebe7fa420ac 100644 --- a/e2e/bug-15141/php74.neon +++ b/e2e/bug-15141/php74.neon @@ -4,3 +4,4 @@ parameters: paths: - test.php - resource-pseudo-type.php + - resource-pseudo-type-errors.php diff --git a/e2e/bug-15141/resource-pseudo-type-errors.php b/e2e/bug-15141/resource-pseudo-type-errors.php new file mode 100644 index 00000000000..f878f64eaeb --- /dev/null +++ b/e2e/bug-15141/resource-pseudo-type-errors.php @@ -0,0 +1,7 @@ +getRawStmtType($function); ++ ++ if ($rawType !== null && self::isResourcePseudoType($rawType)) { ++ $this->addTagToDocComment($function, sprintf('return %s', $rawType)); ++ $function->returnType = null; ++ ++ return; ++ } ++ + $type = $this->getStmtType($function); + + if ($type === null) { +@@ -727,6 +736,15 @@ + + $this->modifyStmtTypeByPhpVersion($parameterNode); + ++ $rawType = $this->getRawStmtType($parameterNode); ++ ++ if ($rawType !== null && self::isResourcePseudoType($rawType)) { ++ assert($parameterNode->var instanceof Node\Expr\Variable); ++ assert(is_string($parameterNode->var->name)); ++ ++ $this->addTagToDocComment($function, sprintf('param %s $%s', $rawType, $parameterNode->var->name)); ++ } ++ + $parameters[] = $parameterNode; + } + +@@ -739,6 +757,20 @@ + */ + private function getStmtType($node) + { ++ $type = $this->getRawStmtType($node); ++ ++ if ($type === null) { ++ return null; ++ } ++ ++ return $this->normalizeType($type); ++ } ++ ++ /** ++ * @param \PhpParser\Node\Stmt\Function_|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Param $node ++ */ ++ private function getRawStmtType($node): ?string ++ { + $languageLevelTypeAwareAttribute = $this->getNodeAttribute($node, 'JetBrains\PhpStorm\Internal\LanguageLevelTypeAware'); + + if ($languageLevelTypeAwareAttribute === null) { +@@ -760,13 +792,13 @@ + continue; + } + +- return $this->normalizeType($type->value->value); ++ return $type->value->value; + } + + assert($languageLevelTypeAwareAttribute->args[1]->value instanceof Node\Scalar\String_); + + return $languageLevelTypeAwareAttribute->args[1]->value->value !== '' +- ? $this->normalizeType($languageLevelTypeAwareAttribute->args[1]->value->value) ++ ? $languageLevelTypeAwareAttribute->args[1]->value->value + : null; + } + +@@ -803,16 +835,7 @@ + $node, + string $annotationName + ): void { +- $docComment = $node->getDocComment(); +- +- if ($docComment === null) { +- $docCommentText = sprintf('/** @%s */', $annotationName); +- } else { +- $docCommentText = preg_replace('~(\r?\n\s*)\*/~', sprintf('\1* @%s\1*/', $annotationName), $docComment->getText()); +- assert($docCommentText !== null); +- } +- +- $node->setDocComment(new Doc($docCommentText)); ++ $this->addTagToDocComment($node, $annotationName); + } + + /** +@@ -991,10 +1014,47 @@ return null; } -+ // `resource` is a pseudo-type, PHP has no such native type - a `resource` -+ // typehint is read back as a class name. Stubs use it in -+ // `#[LanguageLevelTypeAware]` to describe pre-PHP 8 signatures, so drop the -+ // native type there and let the PHPDoc in the same stub describe the type. -+ if (in_array('resource', explode('|', ltrim($type, '?')), true)) { ++ if (self::isResourcePseudoType($type)) { + return null; + } + /** @psalm-suppress InternalClass, InternalMethod */ return BuilderHelpers::normalizeType($type); } + ++ /** ++ * `resource` is a pseudo-type: PHP has no such native type, and a `resource` ++ * typehint in source code is read back as a class name. The stubs use it in ++ * `#[LanguageLevelTypeAware]` to describe pre-PHP 8 signatures, so it must never ++ * become a native type - it is moved into the PHPDoc instead, where `resource` ++ * is a well-known type. ++ */ ++ private static function isResourcePseudoType(string $type): bool ++ { ++ return in_array('resource', explode('|', ltrim($type, '?')), true); ++ } ++ ++ /** ++ * @param \PhpParser\Node\Stmt\ClassLike|\PhpParser\Node\Stmt\ClassConst|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Stmt\Const_|\PhpParser\Node\Stmt\EnumCase $node ++ */ ++ private function addTagToDocComment($node, string $tag): void ++ { ++ $docComment = $node->getDocComment(); ++ ++ if ($docComment === null) { ++ $docCommentText = sprintf('/** @%s */', $tag); ++ } else { ++ $docCommentText = preg_replace_callback( ++ '~(\r?\n\s*)\*/~', ++ static fn (array $matches): string => sprintf('%s* @%s%s*/', $matches[1], $tag, $matches[1]), ++ $docComment->getText() ++ ); ++ assert($docCommentText !== null); ++ } ++ ++ $node->setDocComment(new Doc($docCommentText)); ++ } ++ + private function getStubsDirectory(): string + { + if ($this->stubsDirectory !== null) {