Skip to content
Open
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
38 changes: 29 additions & 9 deletions src/Rules/Comparison/ImpossibleCheckTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public function findSpecifiedType(
Expr $node,
): ?bool
{
$onlyReportFalse = false;
if ($node instanceof FuncCall) {
if ($node->isFirstClassCallable()) {
return null;
Expand All @@ -66,6 +67,17 @@ public function findSpecifiedType(
$argsCount = count($args);
if ($node->name instanceof Node\Name) {
$functionName = strtolower((string) $node->name);
if (in_array($functionName, [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the fact this condition is now useless for 200 lines of code, I feel like it would be better moving the onlyReportFalse declaration.

This way we have

if ($node instanceof FuncCall) {
     // specific logic which could be moved into a private or a dedicated service
}

$onlyReportFalse = ...;

// genericLogic

Maybe one day the

$onlyReportFalse = ...;

will become a

$onlyReportValue = // true/false/null

WDYT @staabm ?

'class_exists',
'interface_exists',
'trait_exists',
'enum_exists',
], true)) {
// Runtime autoload can always fail, so do not report "always true" for these.
// "Always false" is still reportable when the type specifier proves impossibility
// (e.g. class_exists() on a constant string that names an interface).
$onlyReportFalse = true;
}
if ($functionName === 'assert' && $argsCount >= 1) {
$arg = $args[0]->value;
$assertValue = ($this->treatPhpDocTypesAsCertain ? $scope->getType($arg) : $scope->getNativeType($arg))->toBoolean();
Expand All @@ -76,13 +88,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 @@ -284,7 +290,12 @@ public function findSpecifiedType(

$rootExprType = $this->treatPhpDocTypesAsCertain ? $scope->getType($rootExpr) : $scope->getNativeType($rootExpr);
if ($rootExprType instanceof ConstantBooleanType) {
return $rootExprType->getValue();
$value = $rootExprType->getValue();
if ($onlyReportFalse && $value === true) {
return null;
}

return $value;
}

return null;
Expand Down Expand Up @@ -362,7 +373,16 @@ public function findSpecifiedType(
}

$result = TrinaryLogic::createYes()->and(...$results);
return $result->maybe() ? null : $result->yes();
if ($result->maybe()) {
return null;
}

$value = $result->yes();
if ($onlyReportFalse && $value === true) {
return null;
}

return $value;
}

private static function isSpecified(Scope $scope, Expr $node, Expr $expr): bool
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 @@ -284,6 +284,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