Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions src/Rules/Comparison/ImpossibleCheckTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,36 @@ public function findSpecifiedType(
Scope $scope,
Expr $node,
): ?bool
{
$specifiedValue = $this->getSpecifiedType($scope, $node);

/**
* For class_exists()/interface_exists()/trait_exists()/enum_exists() the "always true"
* result is suppressed, because runtime autoload can always fail. "Always false" is still
* reported when the type specifier proves impossibility (e.g. class_exists() on a constant
* string that names an interface).
*/
if (
$specifiedValue === true
&& $node instanceof FuncCall
&& $node->name instanceof Node\Name
&& in_array(strtolower((string) $node->name), [
'class_exists',
'interface_exists',
'trait_exists',
'enum_exists',
], true)
) {
return null;
}

return $specifiedValue;
}

private function getSpecifiedType(
Scope $scope,
Expr $node,
): ?bool
{
if ($node instanceof FuncCall) {
if ($node->isFirstClassCallable()) {
Expand All @@ -76,13 +106,7 @@ public function findSpecifiedType(

return $assertValueIsTrue;
}
if (in_array($functionName, [
'class_exists',
'interface_exists',
'trait_exists',
'enum_exists',
'function_exists',
], true)) {
if ($functionName === 'function_exists') {
return null;
}
if (in_array($functionName, ['count', 'sizeof'], true)) {
Expand Down Expand Up @@ -362,6 +386,7 @@ public function findSpecifiedType(
}

$result = TrinaryLogic::createYes()->and(...$results);

return $result->maybe() ? null : $result->yes();
}

Expand Down
54 changes: 54 additions & 0 deletions src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\Expr\AlwaysRememberedExpr;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\BooleanType;
use PHPStan\Type\ClassStringType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use function count;
use function in_array;
use function ltrim;
Expand All @@ -31,6 +35,10 @@ final class ClassExistsFunctionTypeSpecifyingExtension implements FunctionTypeSp

private TypeSpecifier $typeSpecifier;

public function __construct(private ReflectionProvider $reflectionProvider)
{
}

public function isFunctionSupported(
FunctionReflection $functionReflection,
FuncCall $node,
Expand All @@ -49,6 +57,16 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
{
$args = $node->getArgs();
$argType = $scope->getType($args[0]->value);
$functionName = $functionReflection->getName();

if ($this->isCallAlwaysFalse($functionName, $argType)) {
return $this->typeSpecifier->create(
$args[0]->value,
new NeverType(),
$context,
$scope,
);
}

// class_exists() will only assure one of the classes to exist.
$constantStrings = $argType->getConstantStrings();
Expand Down Expand Up @@ -101,4 +119,40 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
$this->typeSpecifier = $typeSpecifier;
}

private function isCallAlwaysFalse(string $functionName, Type $argType): bool
{
$constantStrings = $argType->getConstantStrings();
if ($constantStrings === []) {
return false;
}

foreach ($constantStrings as $constantString) {
$name = ltrim($constantString->getValue(), '\\');
if (!$this->reflectionProvider->hasClass($name)) {
return false;
}
if ($this->matchesFunctionKind($functionName, $this->reflectionProvider->getClass($name))) {
return false;
}
}

return true;
}

private function matchesFunctionKind(string $functionName, ClassReflection $reflection): bool
{
switch ($functionName) {
case 'class_exists':
return !$reflection->isInterface() && !$reflection->isTrait();
case 'interface_exists':
return $reflection->isInterface();
case 'trait_exists':
return $reflection->isTrait();
case 'enum_exists':
return $reflection->isEnum();
default:
return true;
}
}

}
48 changes: 48 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14683.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug14683;

use function PHPStan\Testing\assertType;

interface SomeInterface {}
class SomeClass {}
trait SomeTrait {}
enum SomeEnum {}

function classExistsOnConstantStringInterface(): void
{
if (class_exists(SomeInterface::class)) {
assertType('*NEVER*', SomeInterface::class);
}
}

function classExistsOnConstantStringTrait(): void
{
if (class_exists(SomeTrait::class)) {
assertType('*NEVER*', SomeTrait::class);
}
}

function interfaceExistsOnConstantStringClass(): void
{
if (interface_exists(SomeClass::class)) {
assertType('*NEVER*', SomeClass::class);
}
}

function enumExistsOnConstantStringEnum(): void
{
if (enum_exists(SomeEnum::class)) {
assertType('\'Bug14683\\\\SomeEnum\'', SomeEnum::class);
}
}

function classExistsOnEnumConstantString(): void
{
// enums are classes in PHP, so this is NOT impossible
if (class_exists(SomeEnum::class)) {
assertType('\'Bug14683\\\\SomeEnum\'', SomeEnum::class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,25 @@ public function testBug7898(): void
$this->analyse([__DIR__ . '/data/bug-7898.php'], []);
}

#[RequiresPhp('>= 8.1.0')]
public function testStructExists(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/check-type-function-call-struct-exists.php'], [
['Call to function class_exists() with \'CheckTypeFunctionCall\\\\_Interface\' will always evaluate to false.', 23],
['Call to function class_exists() with \'CheckTypeFunctionCall\\\\_Trait\' will always evaluate to false.', 25],
['Call to function interface_exists() with \'CheckTypeFunctionCall\\\\_Enum\' will always evaluate to false.', 27],
['Call to function interface_exists() with \'CheckTypeFunctionCall\\\\_Class\' will always evaluate to false.', 29],
['Call to function interface_exists() with \'CheckTypeFunctionCall\\\\_Trait\' will always evaluate to false.', 30],
['Call to function trait_exists() with \'CheckTypeFunctionCall\\\\_Enum\' will always evaluate to false.', 32],
['Call to function trait_exists() with \'CheckTypeFunctionCall\\\\_Interface\' will always evaluate to false.', 33],
['Call to function trait_exists() with \'CheckTypeFunctionCall\\\\_Class\' will always evaluate to false.', 34],
['Call to function enum_exists() with \'CheckTypeFunctionCall\\\\_Interface\' will always evaluate to false.', 38],
['Call to function enum_exists() with \'CheckTypeFunctionCall\\\\_Class\' will always evaluate to false.', 39],
['Call to function enum_exists() with \'CheckTypeFunctionCall\\\\_Trait\' will always evaluate to false.', 40],
]);
}

public function testDoNotReportTypesFromPhpDocs(): void
{
$this->treatPhpDocTypesAsCertain = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php // lint >= 8.1

namespace CheckTypeFunctionCall;

// see https://3v4l.org/V9nmf

interface _Interface {}
class _Class {}
trait _Trait {}
enum _Enum {}

class_exists($mixed);
interface_exists($mixed);
enum_exists($mixed);
trait_exists($mixed);

class_exists();
interface_exists();
enum_exists();
trait_exists();

class_exists(_Enum::class);
class_exists(_Interface::class); // always false
class_exists(_Class::class);
class_exists(_Trait::class); // always false

interface_exists(_Enum::class); // always false
interface_exists(_Interface::class);
interface_exists(_Class::class); // always false
interface_exists(_Trait::class); // always false

trait_exists(_Enum::class); // always false
trait_exists(_Interface::class); // always false
trait_exists(_Class::class); // always false
trait_exists(_Trait::class);

enum_exists(_Enum::class);
enum_exists(_Interface::class); // always false
enum_exists(_Class::class); // always false
enum_exists(_Trait::class); // always false

Loading