Skip to content

Commit 978114e

Browse files
phpstan-botclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 910e8bc commit 978114e

3 files changed

Lines changed: 7 additions & 8 deletions

File tree

resources/functionMetadata.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,6 @@
812812
'chown' => ['hasSideEffects' => true],
813813
'chr' => ['hasSideEffects' => false],
814814
'chunk_split' => ['hasSideEffects' => false],
815-
'class_exists' => ['hasSideEffects' => false],
816815
'class_implements' => ['hasSideEffects' => false],
817816
'class_parents' => ['hasSideEffects' => false],
818817
'cli_get_process_title' => ['hasSideEffects' => true],
@@ -911,7 +910,6 @@
911910
'diskfreespace' => ['hasSideEffects' => true],
912911
'dngettext' => ['hasSideEffects' => false],
913912
'doubleval' => ['hasSideEffects' => false],
914-
'enum_exists' => ['hasSideEffects' => false],
915913
'error_get_last' => ['hasSideEffects' => true],
916914
'error_log' => ['hasSideEffects' => true],
917915
'escapeshellarg' => ['hasSideEffects' => false],
@@ -1192,7 +1190,6 @@
11921190
'ini_get_all' => ['hasSideEffects' => true],
11931191
'intcal_get_maximum' => ['hasSideEffects' => false],
11941192
'intdiv' => ['hasSideEffects' => false],
1195-
'interface_exists' => ['hasSideEffects' => false],
11961193
'intl_error_name' => ['hasSideEffects' => false],
11971194
'intl_get' => ['hasSideEffects' => false],
11981195
'intl_get_error_code' => ['hasSideEffects' => true],
@@ -1726,7 +1723,6 @@
17261723
'token_get_all' => ['hasSideEffects' => false],
17271724
'token_name' => ['hasSideEffects' => false],
17281725
'touch' => ['hasSideEffects' => true],
1729-
'trait_exists' => ['hasSideEffects' => false],
17301726
'transliterator_create' => ['hasSideEffects' => false],
17311727
'transliterator_create_from_rules' => ['hasSideEffects' => false],
17321728
'transliterator_create_inverse' => ['hasSideEffects' => false],

src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
use PHPStan\Analyser\TypeSpecifierAwareExtension;
1313
use PHPStan\Analyser\TypeSpecifierContext;
1414
use PHPStan\DependencyInjection\AutowiredService;
15+
use PHPStan\Node\Expr\AlwaysRememberedExpr;
1516
use PHPStan\Reflection\FunctionReflection;
17+
use PHPStan\Type\BooleanType;
1618
use PHPStan\Type\ClassStringType;
1719
use PHPStan\Type\Constant\ConstantBooleanType;
1820
use PHPStan\Type\Constant\ConstantStringType;
@@ -47,10 +49,11 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
4749
$args = $node->getArgs();
4850
$argType = $scope->getType($args[0]->value);
4951
if ($argType instanceof ConstantStringType) {
52+
$funcCall = new FuncCall(new FullyQualified('class_exists'), [
53+
new Arg(new String_(ltrim($argType->getValue(), '\\'))),
54+
]);
5055
return $this->typeSpecifier->create(
51-
new FuncCall(new FullyQualified('class_exists'), [
52-
new Arg(new String_(ltrim($argType->getValue(), '\\'))),
53-
]),
56+
new AlwaysRememberedExpr($funcCall, new BooleanType(), new BooleanType()),
5457
new ConstantBooleanType(true),
5558
$context,
5659
$scope,

tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function testAutoloadEverythingInFile(): void
5959
$this->assertNotNull($doFooFunctionReflection->getFileName());
6060
$this->assertSame('a.php', basename($doFooFunctionReflection->getFileName()));
6161

62-
$this->assertTrue(class_exists(InCondition::class));
62+
class_exists(InCondition::class);
6363
$classInCondition = $reflector->reflectClass(InCondition::class);
6464
$classInConditionFilename = $classInCondition->getFileName();
6565
$this->assertNotNull($classInConditionFilename);

0 commit comments

Comments
 (0)