Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
6 changes: 0 additions & 6 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1587,12 +1587,6 @@ parameters:
count: 1
path: src/Type/Php/IsAFunctionTypeSpecifyingExtension.php

-
rawMessage: 'Doing instanceof PHPStan\Type\Generic\GenericClassStringType is error-prone and deprecated. Use Type::isClassStringType() and Type::getClassStringObjectType() instead.'
identifier: phpstanApi.instanceofType
count: 2
path: src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php

-
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
identifier: phpstanApi.instanceofType
Expand Down
4 changes: 1 addition & 3 deletions src/Type/Php/IsAFunctionTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
$allowString = !$allowStringType->equals(new ConstantBooleanType(false));

$resultType = $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, true);

// prevent false-positives in IsAFunctionTypeSpecifyingHelper
if ($classType->getConstantStrings() === [] && $resultType->isSuperTypeOf($objectOrClassType)->yes()) {
if ($resultType === null) {
return new SpecifiedTypes([], []);
}

Expand Down
42 changes: 37 additions & 5 deletions src/Type/Php/IsAFunctionTypeSpecifyingHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use PHPStan\Type\UnionType;
use function array_unique;
use function array_values;
use function in_array;

#[AutowiredService]
final class IsAFunctionTypeSpecifyingHelper
Expand All @@ -26,7 +27,7 @@ public function determineType(
Type $classType,
bool $allowString,
bool $allowSameClass,
): Type
): ?Type
{
$objectOrClassTypeClassNames = $objectOrClassType->getObjectClassNames();
if ($allowString) {
Expand All @@ -36,15 +37,39 @@ public function determineType(
$objectOrClassTypeClassNames = array_values(array_unique($objectOrClassTypeClassNames));
}

return TypeTraverser::map(
$isUncertain = $classType->getConstantStrings() === [];

$resultType = TypeTraverser::map(
$classType,
static function (Type $type, callable $traverse) use ($objectOrClassTypeClassNames, $allowString, $allowSameClass): Type {
static function (Type $type, callable $traverse) use ($objectOrClassType, $objectOrClassTypeClassNames, $allowString, $allowSameClass, &$isUncertain): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}
if ($type instanceof ConstantStringType) {
if (!$allowSameClass && $objectOrClassTypeClassNames === [$type->getValue()]) {
return new NeverType();
if (!$allowSameClass) {
if ($objectOrClassTypeClassNames === [$type->getValue()]) {
$isSameClass = true;
foreach ($objectOrClassType->getObjectClassReflections() as $classReflection) {
if (!$classReflection->isFinal()) {
$isSameClass = false;
break;
}
}

if ($isSameClass) {
return new NeverType();
}
}

if (
// For object, as soon as the exact same type is provided
// in the list we cannot be sure of the result
in_array($type->getValue(), $objectOrClassTypeClassNames, true)
// This also occurs for generic class string
|| ($allowString && $objectOrClassTypeClassNames === [] && $objectOrClassType->isSuperTypeOf($type)->yes())
) {
$isUncertain = true;
}
}
if ($allowString) {
return TypeCombinator::union(
Expand Down Expand Up @@ -75,6 +100,13 @@ static function (Type $type, callable $traverse) use ($objectOrClassTypeClassNam
return new ObjectWithoutClassType();
},
);

// prevent false-positives
if ($isUncertain && $resultType->isSuperTypeOf($objectOrClassType)->yes()) {
return null;
}

return $resultType;
}

}
10 changes: 1 addition & 9 deletions src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use PHPStan\Type\Generic\GenericClassStringType;
use function count;
use function strtolower;

Expand Down Expand Up @@ -45,15 +44,8 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
$allowStringType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new ConstantBooleanType(true);
$allowString = !$allowStringType->equals(new ConstantBooleanType(false));

// prevent false-positives in IsAFunctionTypeSpecifyingHelper
if ($objectOrClassType instanceof GenericClassStringType && $classType instanceof GenericClassStringType) {
return new SpecifiedTypes([], []);
}

$resultType = $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, false);

// prevent false-positives in IsAFunctionTypeSpecifyingHelper
if ($classType->getConstantStrings() === [] && $resultType->isSuperTypeOf($objectOrClassType)->yes()) {
if ($resultType === null) {
return new SpecifiedTypes([], []);
}

Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/is-subclass-of.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

function (Bar $a, Bar $b, Bar $c, Bar $d) {
if (is_subclass_of($a, Bar::class)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

do we have a test somewhere for use of is_subclass_of with a final-class ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added

\PHPStan\Testing\assertType('*NEVER*', $a);
\PHPStan\Testing\assertType('IsSubclassOf\Bar', $a); // Can still be a Bar child
}

if (is_subclass_of($b, Foo::class)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,28 @@ public function testBug6305(): void
]);
}

public function testBug6305b(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-6305b.php'], []);
}

public function testBug13713(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-13713.php'], [
[
"Call to function is_subclass_of() with arguments Bug13713\\test, 'stdClass' and false will always evaluate to true.",
12,
],
[
"Call to function is_subclass_of() with arguments class-string<Bug13713\\test>, 'stdClass' and true will always evaluate to true.",
25,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
]);
}

public function testBug6698(): void
{
$this->treatPhpDocTypesAsCertain = true;
Expand Down
28 changes: 28 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-13713.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types = 1);

namespace Bug13713;

function debug(object $object): void {
if ($object instanceof \stdClass) {
echo var_export(\is_subclass_of($object, \stdClass::class, false), true) . \PHP_EOL;
}
if ($object instanceof test) {
echo var_export(\is_subclass_of($object, \stdClass::class, false), true) . \PHP_EOL;
}
}

class test extends \stdClass {}
debug(new test);

/**
* @param class-string<\stdClass> $stdClass
* @param class-string<test> $test
*/
function debugWithClass(string $stdClass, string $test): void {
echo var_export(\is_subclass_of($stdClass, \stdClass::class, true), true) . \PHP_EOL;
echo var_export(\is_subclass_of($test, \stdClass::class, true), true) . \PHP_EOL;
}

debugWithClass(test::class, test::class);
23 changes: 23 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-6305b.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Bug6305b;

class A {}

class B extends A {}

$b = mt_rand(0, 1) === 0 ? new B() : new A();

if (is_subclass_of($b, A::class)) {
}

if (is_subclass_of($b, B::class)) {
}

$b = mt_rand(0, 1) === 0 ? A::class : B::class;

if (is_subclass_of($b, A::class)) {
}

if (is_subclass_of($b, B::class)) {
}
Loading