From 910e8bc469448fa04a67d2640e20c27a0b3ab920 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Wed, 6 May 2026 19:43:32 +0000 Subject: [PATCH 1/9] Mark `class_exists`, `interface_exists`, `trait_exists`, and `enum_exists` as having no side effects in function metadata - Add `class_exists`, `interface_exists`, `trait_exists`, and `enum_exists` to `resources/functionMetadata.php` with `hasSideEffects => false`, matching the existing `function_exists` entry - When `rememberPossiblyImpureFunctionValues` was false, the type narrowing from these functions was blocked in `TypeSpecifier::createForExpr()` because they lacked metadata and defaulted to `hasSideEffects = maybe` - Update `AutoloadSourceLocatorTest` to use the return value of `class_exists()` to avoid the now-triggered `function.resultUnused` rule --- resources/functionMetadata.php | 4 +++ tests/PHPStan/Analyser/Bug8579Test.php | 31 +++++++++++++++++++ tests/PHPStan/Analyser/data/bug-8579.php | 4 +++ .../AutoloadSourceLocatorTest.php | 2 +- 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/Bug8579Test.php create mode 100644 tests/PHPStan/Analyser/data/bug-8579.php diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index 4e50a9f80f3..16c20dd8401 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -812,6 +812,7 @@ 'chown' => ['hasSideEffects' => true], 'chr' => ['hasSideEffects' => false], 'chunk_split' => ['hasSideEffects' => false], + 'class_exists' => ['hasSideEffects' => false], 'class_implements' => ['hasSideEffects' => false], 'class_parents' => ['hasSideEffects' => false], 'cli_get_process_title' => ['hasSideEffects' => true], @@ -910,6 +911,7 @@ 'diskfreespace' => ['hasSideEffects' => true], 'dngettext' => ['hasSideEffects' => false], 'doubleval' => ['hasSideEffects' => false], + 'enum_exists' => ['hasSideEffects' => false], 'error_get_last' => ['hasSideEffects' => true], 'error_log' => ['hasSideEffects' => true], 'escapeshellarg' => ['hasSideEffects' => false], @@ -1190,6 +1192,7 @@ 'ini_get_all' => ['hasSideEffects' => true], 'intcal_get_maximum' => ['hasSideEffects' => false], 'intdiv' => ['hasSideEffects' => false], + 'interface_exists' => ['hasSideEffects' => false], 'intl_error_name' => ['hasSideEffects' => false], 'intl_get' => ['hasSideEffects' => false], 'intl_get_error_code' => ['hasSideEffects' => true], @@ -1723,6 +1726,7 @@ 'token_get_all' => ['hasSideEffects' => false], 'token_name' => ['hasSideEffects' => false], 'touch' => ['hasSideEffects' => true], + 'trait_exists' => ['hasSideEffects' => false], 'transliterator_create' => ['hasSideEffects' => false], 'transliterator_create_from_rules' => ['hasSideEffects' => false], 'transliterator_create_inverse' => ['hasSideEffects' => false], diff --git a/tests/PHPStan/Analyser/Bug8579Test.php b/tests/PHPStan/Analyser/Bug8579Test.php new file mode 100644 index 00000000000..be303ecaa3b --- /dev/null +++ b/tests/PHPStan/Analyser/Bug8579Test.php @@ -0,0 +1,31 @@ +getFileHelper()->normalizePath(__DIR__ . '/data/bug-8579.php'); + + $analyser = self::getContainer()->getByType(Analyser::class); + $finalizer = self::getContainer()->getByType(AnalyserResultFinalizer::class); + $errors = $finalizer->finalize( + $analyser->analyse([$file], null, null, true), + false, + true, + )->getErrors(); + $this->assertNoErrors($errors); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/do-not-remember-possibly-impure-function-values.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-8579.php b/tests/PHPStan/Analyser/data/bug-8579.php new file mode 100644 index 00000000000..df23526fa58 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8579.php @@ -0,0 +1,4 @@ +assertNotNull($doFooFunctionReflection->getFileName()); $this->assertSame('a.php', basename($doFooFunctionReflection->getFileName())); - class_exists(InCondition::class); + $this->assertTrue(class_exists(InCondition::class)); $classInCondition = $reflector->reflectClass(InCondition::class); $classInConditionFilename = $classInCondition->getFileName(); $this->assertNotNull($classInConditionFilename); From 978114e6a924ff80b2f33f34247e269f887467d7 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 6 May 2026 20:53:54 +0000 Subject: [PATCH 2/9] Use AlwaysRememberedExpr to bypass impurity check for class_exists type narrowing Instead of incorrectly marking class_exists, interface_exists, trait_exists, and enum_exists as having no side effects (they trigger autoloading), wrap the FuncCall in AlwaysRememberedExpr in ClassExistsFunctionTypeSpecifyingExtension. This bypasses the rememberPossiblyImpureFunctionValues check in TypeSpecifier::createForExpr() while keeping the correct hasSideEffects semantics. Co-Authored-By: Claude Opus 4.6 --- resources/functionMetadata.php | 4 ---- .../Php/ClassExistsFunctionTypeSpecifyingExtension.php | 9 ++++++--- .../SourceLocator/AutoloadSourceLocatorTest.php | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index 16c20dd8401..4e50a9f80f3 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -812,7 +812,6 @@ 'chown' => ['hasSideEffects' => true], 'chr' => ['hasSideEffects' => false], 'chunk_split' => ['hasSideEffects' => false], - 'class_exists' => ['hasSideEffects' => false], 'class_implements' => ['hasSideEffects' => false], 'class_parents' => ['hasSideEffects' => false], 'cli_get_process_title' => ['hasSideEffects' => true], @@ -911,7 +910,6 @@ 'diskfreespace' => ['hasSideEffects' => true], 'dngettext' => ['hasSideEffects' => false], 'doubleval' => ['hasSideEffects' => false], - 'enum_exists' => ['hasSideEffects' => false], 'error_get_last' => ['hasSideEffects' => true], 'error_log' => ['hasSideEffects' => true], 'escapeshellarg' => ['hasSideEffects' => false], @@ -1192,7 +1190,6 @@ 'ini_get_all' => ['hasSideEffects' => true], 'intcal_get_maximum' => ['hasSideEffects' => false], 'intdiv' => ['hasSideEffects' => false], - 'interface_exists' => ['hasSideEffects' => false], 'intl_error_name' => ['hasSideEffects' => false], 'intl_get' => ['hasSideEffects' => false], 'intl_get_error_code' => ['hasSideEffects' => true], @@ -1726,7 +1723,6 @@ 'token_get_all' => ['hasSideEffects' => false], 'token_name' => ['hasSideEffects' => false], 'touch' => ['hasSideEffects' => true], - 'trait_exists' => ['hasSideEffects' => false], 'transliterator_create' => ['hasSideEffects' => false], 'transliterator_create_from_rules' => ['hasSideEffects' => false], 'transliterator_create_inverse' => ['hasSideEffects' => false], diff --git a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php index 8265a598b1f..06c1aad5937 100644 --- a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php @@ -12,7 +12,9 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\Expr\AlwaysRememberedExpr; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\BooleanType; use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; @@ -47,10 +49,11 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $args = $node->getArgs(); $argType = $scope->getType($args[0]->value); if ($argType instanceof ConstantStringType) { + $funcCall = new FuncCall(new FullyQualified('class_exists'), [ + new Arg(new String_(ltrim($argType->getValue(), '\\'))), + ]); return $this->typeSpecifier->create( - new FuncCall(new FullyQualified('class_exists'), [ - new Arg(new String_(ltrim($argType->getValue(), '\\'))), - ]), + new AlwaysRememberedExpr($funcCall, new BooleanType(), new BooleanType()), new ConstantBooleanType(true), $context, $scope, diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php index 9dbbfe7e7b4..522bc15e224 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php @@ -59,7 +59,7 @@ public function testAutoloadEverythingInFile(): void $this->assertNotNull($doFooFunctionReflection->getFileName()); $this->assertSame('a.php', basename($doFooFunctionReflection->getFileName())); - $this->assertTrue(class_exists(InCondition::class)); + class_exists(InCondition::class); $classInCondition = $reflector->reflectClass(InCondition::class); $classInConditionFilename = $classInCondition->getFileName(); $this->assertNotNull($classInConditionFilename); From 50a27565192b25474a6445aad4b32d7ca4f14733 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 6 May 2026 21:14:59 +0000 Subject: [PATCH 3/9] Add test ensuring class_exists false result is not always remembered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When rememberPossiblyImpureFunctionValues is false, a false result from class_exists() must not be permanently cached — a subsequent class_exists() call for the same class should still be able to return true (since the class could be autoloaded between calls). Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/Bug8579Test.php | 14 ++++++++++ .../data/bug-8579-false-not-remembered.php | 28 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/PHPStan/Analyser/data/bug-8579-false-not-remembered.php diff --git a/tests/PHPStan/Analyser/Bug8579Test.php b/tests/PHPStan/Analyser/Bug8579Test.php index be303ecaa3b..ae924be21a7 100644 --- a/tests/PHPStan/Analyser/Bug8579Test.php +++ b/tests/PHPStan/Analyser/Bug8579Test.php @@ -21,6 +21,20 @@ public function testBug8579(): void $this->assertNoErrors($errors); } + public function testClassExistsFalseNotAlwaysRemembered(): void + { + $file = $this->getFileHelper()->normalizePath(__DIR__ . '/data/bug-8579-false-not-remembered.php'); + + $analyser = self::getContainer()->getByType(Analyser::class); + $finalizer = self::getContainer()->getByType(AnalyserResultFinalizer::class); + $errors = $finalizer->finalize( + $analyser->analyse([$file], null, null, true), + false, + true, + )->getErrors(); + $this->assertNoErrors($errors); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Analyser/data/bug-8579-false-not-remembered.php b/tests/PHPStan/Analyser/data/bug-8579-false-not-remembered.php new file mode 100644 index 00000000000..cbe4bb7c74f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8579-false-not-remembered.php @@ -0,0 +1,28 @@ + Date: Thu, 7 May 2026 05:57:28 +0000 Subject: [PATCH 4/9] Convert Bug8579Test to standard rule test and assertType test Move the Bug8579 regression test from a custom Analyser test to a standard RuleTestCase for InstantiationRule with rememberPossiblyImpureFunctionValues: false config. Convert the class_exists false-not-remembered test to assertType assertions in the existing DoNotRememberPossiblyImpureFunctionValues test data file. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/Bug8579Test.php | 45 ------------------- .../data/bug-8579-false-not-remembered.php | 28 ------------ ...member-possibly-impure-function-values.php | 20 +++++++++ ...otRememberPossiblyImpureValuesRuleTest.php | 35 +++++++++++++++ .../Classes}/data/bug-8579.php | 0 .../doNotRememberPossiblyImpureValues.neon | 2 + 6 files changed, 57 insertions(+), 73 deletions(-) delete mode 100644 tests/PHPStan/Analyser/Bug8579Test.php delete mode 100644 tests/PHPStan/Analyser/data/bug-8579-false-not-remembered.php create mode 100644 tests/PHPStan/Rules/Classes/InstantiationDoNotRememberPossiblyImpureValuesRuleTest.php rename tests/PHPStan/{Analyser => Rules/Classes}/data/bug-8579.php (100%) create mode 100644 tests/PHPStan/Rules/Classes/doNotRememberPossiblyImpureValues.neon diff --git a/tests/PHPStan/Analyser/Bug8579Test.php b/tests/PHPStan/Analyser/Bug8579Test.php deleted file mode 100644 index ae924be21a7..00000000000 --- a/tests/PHPStan/Analyser/Bug8579Test.php +++ /dev/null @@ -1,45 +0,0 @@ -getFileHelper()->normalizePath(__DIR__ . '/data/bug-8579.php'); - - $analyser = self::getContainer()->getByType(Analyser::class); - $finalizer = self::getContainer()->getByType(AnalyserResultFinalizer::class); - $errors = $finalizer->finalize( - $analyser->analyse([$file], null, null, true), - false, - true, - )->getErrors(); - $this->assertNoErrors($errors); - } - - public function testClassExistsFalseNotAlwaysRemembered(): void - { - $file = $this->getFileHelper()->normalizePath(__DIR__ . '/data/bug-8579-false-not-remembered.php'); - - $analyser = self::getContainer()->getByType(Analyser::class); - $finalizer = self::getContainer()->getByType(AnalyserResultFinalizer::class); - $errors = $finalizer->finalize( - $analyser->analyse([$file], null, null, true), - false, - true, - )->getErrors(); - $this->assertNoErrors($errors); - } - - public static function getAdditionalConfigFiles(): array - { - return [ - __DIR__ . '/do-not-remember-possibly-impure-function-values.neon', - ]; - } - -} diff --git a/tests/PHPStan/Analyser/data/bug-8579-false-not-remembered.php b/tests/PHPStan/Analyser/data/bug-8579-false-not-remembered.php deleted file mode 100644 index cbe4bb7c74f..00000000000 --- a/tests/PHPStan/Analyser/data/bug-8579-false-not-remembered.php +++ /dev/null @@ -1,28 +0,0 @@ - + */ +class InstantiationDoNotRememberPossiblyImpureValuesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(InstantiationRule::class); + } + + public function testBug8579(): void + { + $this->analyse([__DIR__ . '/data/bug-8579.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/doNotRememberPossiblyImpureValues.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-8579.php b/tests/PHPStan/Rules/Classes/data/bug-8579.php similarity index 100% rename from tests/PHPStan/Analyser/data/bug-8579.php rename to tests/PHPStan/Rules/Classes/data/bug-8579.php diff --git a/tests/PHPStan/Rules/Classes/doNotRememberPossiblyImpureValues.neon b/tests/PHPStan/Rules/Classes/doNotRememberPossiblyImpureValues.neon new file mode 100644 index 00000000000..971a1184f48 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/doNotRememberPossiblyImpureValues.neon @@ -0,0 +1,2 @@ +parameters: + rememberPossiblyImpureFunctionValues: false From 3567b003c82180d59cc0c90abf49da8ca5224abb Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 07:13:43 +0000 Subject: [PATCH 5/9] Add assertType inside if branches for class_exists do-not-remember test Co-Authored-By: Claude Opus 4.6 --- .../do-not-remember-possibly-impure-function-values.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php b/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php index 4348709f930..aeea21fce2f 100644 --- a/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php +++ b/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php @@ -113,7 +113,7 @@ function test(): void function testClassExistsFalseNotRemembered(): void { if (!class_exists('Bug8579FalseNotRememberedA')) { - // class_exists returned false here, but we don't exit + assertType('bool', class_exists('Bug8579FalseNotRememberedA')); } assertType('bool', class_exists('Bug8579FalseNotRememberedA')); @@ -122,9 +122,9 @@ function testClassExistsFalseNotRemembered(): void function testClassExistsFalseNotRememberedElse(): void { if (class_exists('Bug8579FalseNotRememberedB')) { - // true branch + assertType('bool', class_exists('Bug8579FalseNotRememberedB')); } else { - // class_exists returned false in this branch + assertType('bool', class_exists('Bug8579FalseNotRememberedB')); } assertType('bool', class_exists('Bug8579FalseNotRememberedB')); From 327da3346f34697dbf761d18005e1059f0ec8dcf Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 08:58:50 +0000 Subject: [PATCH 6/9] Add interface_exists/trait_exists/enum_exists tests and fix nsrt assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add interface_exists, trait_exists, and enum_exists guard tests to bug-8579.php rule test data, matching the existing class_exists test - Fix nsrt assertions in do-not-remember test to use fully qualified \class_exists() so the expression key matches the stored type from AlwaysRememberedExpr — the truthy-branch assertType('true') now correctly fails without the fix and passes with it - Replace tests that passed trivially (asserting 'bool' with unqualified class_exists, which never matched the stored key) Co-Authored-By: Claude Opus 4.6 --- ...member-possibly-impure-function-values.php | 20 +++++++++---------- tests/PHPStan/Rules/Classes/data/bug-8579.php | 9 +++++++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php b/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php index aeea21fce2f..41b38f4f22e 100644 --- a/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php +++ b/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php @@ -110,22 +110,22 @@ function test(): void } } -function testClassExistsFalseNotRemembered(): void +function testClassExistsRemembered(): void { - if (!class_exists('Bug8579FalseNotRememberedA')) { - assertType('bool', class_exists('Bug8579FalseNotRememberedA')); + if (\class_exists('Bug8579RememberedA')) { + assertType('true', \class_exists('Bug8579RememberedA')); + } else { + assertType('bool', \class_exists('Bug8579RememberedA')); } - assertType('bool', class_exists('Bug8579FalseNotRememberedA')); + assertType('bool', \class_exists('Bug8579RememberedA')); } -function testClassExistsFalseNotRememberedElse(): void +function testClassExistsFalseNotRemembered(): void { - if (class_exists('Bug8579FalseNotRememberedB')) { - assertType('bool', class_exists('Bug8579FalseNotRememberedB')); - } else { - assertType('bool', class_exists('Bug8579FalseNotRememberedB')); + if (!\class_exists('Bug8579FalseNotRememberedA')) { + assertType('bool', \class_exists('Bug8579FalseNotRememberedA')); } - assertType('bool', class_exists('Bug8579FalseNotRememberedB')); + assertType('bool', \class_exists('Bug8579FalseNotRememberedA')); } diff --git a/tests/PHPStan/Rules/Classes/data/bug-8579.php b/tests/PHPStan/Rules/Classes/data/bug-8579.php index df23526fa58..018ab54678c 100644 --- a/tests/PHPStan/Rules/Classes/data/bug-8579.php +++ b/tests/PHPStan/Rules/Classes/data/bug-8579.php @@ -2,3 +2,12 @@ if (!class_exists('NonexistentClassBug8579')) throw new \Exception('nonexistentclass'); $x = new \NonexistentClassBug8579(); + +if (!interface_exists('NonexistentInterfaceBug8579')) throw new \Exception('nonexistentinterface'); +$x = new \NonexistentInterfaceBug8579(); + +if (!trait_exists('NonexistentTraitBug8579')) throw new \Exception('nonexistenttrait'); +$x = new \NonexistentTraitBug8579(); + +if (!enum_exists('NonexistentEnumBug8579')) throw new \Exception('nonexistentenum'); +$x = new \NonexistentEnumBug8579(); From 0d8b316eb6cbc27490e5c02feeb2ac14c4780ccf Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 09:04:50 +0000 Subject: [PATCH 7/9] Document AlwaysRememberedExpr with class-level PHPDoc Co-Authored-By: Claude Opus 4.6 --- src/Node/Expr/AlwaysRememberedExpr.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Node/Expr/AlwaysRememberedExpr.php b/src/Node/Expr/AlwaysRememberedExpr.php index 71f40b97244..c62202a164a 100644 --- a/src/Node/Expr/AlwaysRememberedExpr.php +++ b/src/Node/Expr/AlwaysRememberedExpr.php @@ -7,6 +7,20 @@ use PHPStan\Node\VirtualNode; use PHPStan\Type\Type; +/** + * Wraps an expression so its type is remembered in the scope even when + * `rememberPossiblyImpureFunctionValues` is false. + * + * TypeSpecifier::createForExpr() returns empty SpecifiedTypes for impure + * function calls when that setting is off. Wrapping the call in this node + * bypasses that check (since AlwaysRememberedExpr is not a FuncCall) while + * MutatingScope::specifyExpressionType() propagates the type to the inner + * expression as well. + * + * Used for function calls whose result should always participate in type + * narrowing regardless of purity — e.g. class_exists() guards that gate + * "class not found" errors. + */ final class AlwaysRememberedExpr extends Expr implements VirtualNode { From 60bcc54c1cf984e2c9c8e8444117134641a539fb Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 09:04:56 +0000 Subject: [PATCH 8/9] Add tests for interface_exists, trait_exists, and enum_exists with rememberPossiblyImpureFunctionValues: false Co-Authored-By: Claude Opus 4.6 --- ...member-possibly-impure-function-values.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php b/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php index 41b38f4f22e..158ab83dec7 100644 --- a/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php +++ b/tests/PHPStan/Analyser/data/do-not-remember-possibly-impure-function-values.php @@ -129,3 +129,30 @@ function testClassExistsFalseNotRemembered(): void assertType('bool', \class_exists('Bug8579FalseNotRememberedA')); } + +function testInterfaceExistsFalseNotRemembered(): void +{ + if (!\interface_exists('Bug8579FalseNotRememberedC')) { + assertType('bool', \interface_exists('Bug8579FalseNotRememberedC')); + } + + assertType('bool', \interface_exists('Bug8579FalseNotRememberedC')); +} + +function testTraitExistsFalseNotRemembered(): void +{ + if (!\trait_exists('Bug8579FalseNotRememberedD')) { + assertType('bool', \trait_exists('Bug8579FalseNotRememberedD')); + } + + assertType('bool', \trait_exists('Bug8579FalseNotRememberedD')); +} + +function testEnumExistsFalseNotRemembered(): void +{ + if (!\enum_exists('Bug8579FalseNotRememberedE')) { + assertType('bool', \enum_exists('Bug8579FalseNotRememberedE')); + } + + assertType('bool', \enum_exists('Bug8579FalseNotRememberedE')); +} From 36db3d4a8d2571a4e4608985db7f9b0ada1f3bb7 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 7 May 2026 13:34:15 +0000 Subject: [PATCH 9/9] Shorten AlwaysRememberedExpr PHPDoc to be generic about its purpose Co-Authored-By: Claude Opus 4.6 --- src/Node/Expr/AlwaysRememberedExpr.php | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/Node/Expr/AlwaysRememberedExpr.php b/src/Node/Expr/AlwaysRememberedExpr.php index c62202a164a..f389fac356b 100644 --- a/src/Node/Expr/AlwaysRememberedExpr.php +++ b/src/Node/Expr/AlwaysRememberedExpr.php @@ -7,20 +7,7 @@ use PHPStan\Node\VirtualNode; use PHPStan\Type\Type; -/** - * Wraps an expression so its type is remembered in the scope even when - * `rememberPossiblyImpureFunctionValues` is false. - * - * TypeSpecifier::createForExpr() returns empty SpecifiedTypes for impure - * function calls when that setting is off. Wrapping the call in this node - * bypasses that check (since AlwaysRememberedExpr is not a FuncCall) while - * MutatingScope::specifyExpressionType() propagates the type to the inner - * expression as well. - * - * Used for function calls whose result should always participate in type - * narrowing regardless of purity — e.g. class_exists() guards that gate - * "class not found" errors. - */ +/** Wraps an expression so its type is always remembered in the scope, bypassing impurity checks. */ final class AlwaysRememberedExpr extends Expr implements VirtualNode {