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
3 changes: 2 additions & 1 deletion src/Type/Php/IsAFunctionTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
if (count($node->getArgs()) < 2) {
return new SpecifiedTypes();
}
$objectOrClassType = $scope->getType($node->getArgs()[0]->value);
$classType = $scope->getType($node->getArgs()[1]->value);
$allowStringType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new ConstantBooleanType(false);
$allowString = !$allowStringType->equals(new ConstantBooleanType(false));
Expand All @@ -47,7 +48,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n

return $this->typeSpecifier->create(
$node->getArgs()[0]->value,
$this->isAFunctionTypeSpecifyingHelper->determineType($classType, $allowString),
$this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, true),
$context,
false,
$scope,
Expand Down
24 changes: 23 additions & 1 deletion src/Type/Php/IsAFunctionTypeSpecifyingHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,37 @@
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\UnionType;

final class IsAFunctionTypeSpecifyingHelper
{

public function determineType(
Type $objectOrClassType,
Type $classType,
bool $allowString,
bool $allowSameClass,
): Type
{
$objectOrClassTypeClassName = $this->determineClassNameFromObjectOrClassType($objectOrClassType, $allowString);

return TypeTraverser::map(
$classType,
static function (Type $type, callable $traverse) use ($allowString): Type {
static function (Type $type, callable $traverse) use ($objectOrClassTypeClassName, $allowString, $allowSameClass): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}
if ($type instanceof ConstantStringType) {
if (!$allowSameClass && $type->getValue() === $objectOrClassTypeClassName) {
return new NeverType();
}
if ($allowString) {
return TypeCombinator::union(
new ObjectType($type->getValue()),
Expand Down Expand Up @@ -59,4 +68,17 @@ static function (Type $type, callable $traverse) use ($allowString): Type {
);
}

private function determineClassNameFromObjectOrClassType(Type $type, bool $allowString): ?string
{
if ($type instanceof TypeWithClassName) {
return $type->getClassName();
}

if ($allowString && $type instanceof ConstantStringType) {
return $type->getValue();
}

return null;
}

}
3 changes: 2 additions & 1 deletion src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
if (count($node->getArgs()) < 2) {
return new SpecifiedTypes();
}
$objectOrClassType = $scope->getType($node->getArgs()[0]->value);
$classType = $scope->getType($node->getArgs()[1]->value);
$allowStringType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new ConstantBooleanType(true);
$allowString = !$allowStringType->equals(new ConstantBooleanType(false));
Expand All @@ -47,7 +48,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n

return $this->typeSpecifier->create(
$node->getArgs()[0]->value,
$this->isAFunctionTypeSpecifyingHelper->determineType($classType, $allowString),
$this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, false),
$context,
false,
$scope,
Expand Down
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public function dataFileAsserts(): iterable
}
yield from $this->gatherAssertTypes(__DIR__ . '/data/is-numeric.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/is-a.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/is-subclass-of.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3142.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-shapes-keys-strings.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1216.php');
Expand Down Expand Up @@ -754,6 +755,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6505.php');
}

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6305.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6699.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6715.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6682.php');
Expand Down
19 changes: 19 additions & 0 deletions tests/PHPStan/Analyser/data/bug-6305.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php declare(strict_types = 1);

namespace Bug6305;

use function PHPStan\Testing\assertType;

class A {}

class B extends A {}

$b = new B();

if (is_subclass_of($b, A::class)) {
assertType('Bug6305\B', $b);
}

if (is_subclass_of($b, B::class)) {
assertType('*NEVER*', $b);
}
60 changes: 57 additions & 3 deletions tests/PHPStan/Analyser/data/is-a.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<?php

namespace IsA;

function (object $foo) {
if (is_a($foo, Foo::class)) {
\PHPStan\Testing\assertType('Foo', $foo);
\PHPStan\Testing\assertType('IsA\Foo', $foo);
}
};

Expand All @@ -11,13 +13,13 @@ function (object $foo) {
$fooClassString = 'Foo';

if (is_a($foo, $fooClassString)) {
\PHPStan\Testing\assertType('Foo', $foo);
\PHPStan\Testing\assertType('IsA\Foo', $foo);
}
};

function (string $foo) {
if (is_a($foo, Foo::class, true)) {
\PHPStan\Testing\assertType('class-string<Foo>', $foo);
\PHPStan\Testing\assertType('class-string<IsA\Foo>', $foo);
}
};

Expand All @@ -26,3 +28,55 @@ function (string $foo, string $someString) {
\PHPStan\Testing\assertType('class-string', $foo);
}
};

function (Bar $a, Bar $b, Bar $c, Bar $d) {
if (is_a($a, Bar::class)) {
\PHPStan\Testing\assertType('IsA\Bar', $a);
}

if (is_a($b, Foo::class)) {
\PHPStan\Testing\assertType('IsA\Bar', $b);
}

/** @var class-string<Bar> $barClassString */
$barClassString = 'Bar';
if (is_a($c, $barClassString)) {
\PHPStan\Testing\assertType('IsA\Bar', $c);
}

/** @var class-string<Foo> $fooClassString */
$fooClassString = 'Foo';
if (is_a($d, $fooClassString)) {
\PHPStan\Testing\assertType('IsA\Bar', $d);
}
};

function (string $a, string $b, string $c, string $d) {
/** @var class-string<Bar> $a */
if (is_a($a, Bar::class, true)) {
\PHPStan\Testing\assertType('class-string<IsA\Bar>', $a);
}

/** @var class-string<Bar> $b */
if (is_a($b, Foo::class, true)) {
\PHPStan\Testing\assertType('class-string<IsA\Bar>', $b);
}

/** @var class-string<Bar> $c */
/** @var class-string<Bar> $barClassString */
$barClassString = 'Bar';
if (is_a($c, $barClassString, true)) {
\PHPStan\Testing\assertType('class-string<IsA\Bar>', $c);
}

/** @var class-string<Bar> $d */
/** @var class-string<Foo> $fooClassString */
$fooClassString = 'Foo';
if (is_a($d, $fooClassString, true)) {
\PHPStan\Testing\assertType('class-string<IsA\Bar>', $d);
}
};

class Foo {}

class Bar extends Foo {}
55 changes: 55 additions & 0 deletions tests/PHPStan/Analyser/data/is-subclass-of.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace IsSubclassOf;

function (Bar $a, Bar $b, Bar $c, Bar $d) {
if (is_subclass_of($a, Bar::class)) {
\PHPStan\Testing\assertType('*NEVER*', $a);
}

if (is_subclass_of($b, Foo::class)) {
\PHPStan\Testing\assertType('IsSubclassOf\Bar', $b);
}

/** @var class-string<Bar> $barClassString */
$barClassString = 'Bar';
if (is_subclass_of($c, $barClassString)) {
\PHPStan\Testing\assertType('IsSubclassOf\Bar', $c);
}

/** @var class-string<Foo> $fooClassString */
$fooClassString = 'Foo';
if (is_subclass_of($d, $fooClassString)) {
\PHPStan\Testing\assertType('IsSubclassOf\Bar', $d);
}
};

function (string $a, string $b, string $c, string $d) {
/** @var class-string<Bar> $a */
if (is_subclass_of($a, Bar::class)) {
\PHPStan\Testing\assertType('class-string<IsSubclassOf\Bar>', $a);
}

/** @var class-string<Bar> $b */
if (is_subclass_of($b, Foo::class)) {
\PHPStan\Testing\assertType('class-string<IsSubclassOf\Bar>', $b);
}

/** @var class-string<Bar> $c */
/** @var class-string<Bar> $barClassString */
$barClassString = 'Bar';
if (is_subclass_of($c, $barClassString)) {
\PHPStan\Testing\assertType('class-string<IsSubclassOf\Bar>', $c);
}

/** @var class-string<Bar> $d */
/** @var class-string<Foo> $fooClassString */
$fooClassString = 'Foo';
if (is_subclass_of($d, $fooClassString)) {
\PHPStan\Testing\assertType('class-string<IsSubclassOf\Bar>', $d);
}
};

class Foo {}

class Bar extends Foo {}
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,22 @@ public function testBug3766(): void
$this->analyse([__DIR__ . '/data/bug-3766.php'], []);
}

public function testBug6305(): void
{
$this->checkAlwaysTrueCheckTypeFunctionCall = true;
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-6305.php'], [
[
'Call to function is_subclass_of() with Bug6305\B and \'Bug6305\\\A\' will always evaluate to true.',
11,
],
[
'Call to function is_subclass_of() with Bug6305\B and \'Bug6305\\\B\' will always evaluate to false.',
14,
],
]);
}

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

namespace Bug6305;

class A {}

class B extends A {}

$b = new B();

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

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