Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Specify isA class-string types for falsey context #1040

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions conf/config.neon
Expand Up @@ -1438,6 +1438,9 @@ services:
tags:
- phpstan.typeSpecifier.functionTypeSpecifyingExtension

-
class: PHPStan\Type\Php\IsAFunctionTypeSpecifyingHelper

-
class: PHPStan\Type\Php\ArrayIsListFunctionTypeSpecifyingExtension
tags:
Expand Down
67 changes: 21 additions & 46 deletions src/Type/Php/IsAFunctionTypeSpecifyingExtension.php
Expand Up @@ -2,81 +2,56 @@

namespace PHPStan\Type\Php;

use PhpParser\Node;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\ClassStringType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use function count;
use function strtolower;

class IsAFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
{

private TypeSpecifier $typeSpecifier;

public function __construct(
private IsAFunctionTypeSpecifyingHelper $isAFunctionTypeSpecifyingHelper,
)
{
}

public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool
{
return strtolower($functionReflection->getName()) === 'is_a'
&& isset($node->getArgs()[0])
&& isset($node->getArgs()[1])
&& !$context->null();
}

public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
{
if ($context->null()) {
throw new ShouldNotHappenException();
}

$classNameArgExpr = $node->getArgs()[1]->value;
$classNameArgExprType = $scope->getType($classNameArgExpr);
if (
$classNameArgExpr instanceof ClassConstFetch
&& $classNameArgExpr->class instanceof Name
&& $classNameArgExpr->name instanceof Node\Identifier
&& strtolower($classNameArgExpr->name->name) === 'class'
) {
$objectType = $scope->resolveTypeByName($classNameArgExpr->class);
$types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope);
} elseif ($classNameArgExprType instanceof ConstantStringType) {
$objectType = new ObjectType($classNameArgExprType->getValue());
$types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope);
} elseif ($classNameArgExprType instanceof GenericClassStringType) {
$objectType = $classNameArgExprType->getGenericType();
$types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope);
} elseif ($context->true()) {
$objectType = new ObjectWithoutClassType();
$types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope);
} else {
$types = new SpecifiedTypes();
if (count($node->getArgs()) < 2) {
return new SpecifiedTypes();
}
$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));

if (isset($node->getArgs()[2]) && $context->true()) {
if (!$scope->getType($node->getArgs()[2]->value)->isSuperTypeOf(new ConstantBooleanType(true))->no()) {
$types = $types->intersectWith($this->typeSpecifier->create(
$node->getArgs()[0]->value,
isset($objectType) ? new GenericClassStringType($objectType) : new ClassStringType(),
$context,
false,
$scope,
));
}
if (!$classType instanceof ConstantStringType && !$context->truthy()) {
return new SpecifiedTypes([], []);
}

return $types;
return $this->typeSpecifier->create(
$node->getArgs()[0]->value,
$this->isAFunctionTypeSpecifyingHelper->determineType($classType, $allowString),
$context,
false,
$scope,
);
}

public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
Expand Down
62 changes: 62 additions & 0 deletions src/Type/Php/IsAFunctionTypeSpecifyingHelper.php
@@ -0,0 +1,62 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PHPStan\Type\ClassStringType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;

final class IsAFunctionTypeSpecifyingHelper
{

public function determineType(
Type $classType,
bool $allowString,
): Type
{
return TypeTraverser::map(
$classType,
static function (Type $type, callable $traverse) use ($allowString): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}
if ($type instanceof ConstantStringType) {
if ($allowString) {
return TypeCombinator::union(
new ObjectType($type->getValue()),
new GenericClassStringType(new ObjectType($type->getValue())),
);
}

return new ObjectType($type->getValue());
}
if ($type instanceof GenericClassStringType) {
if ($allowString) {
return TypeCombinator::union(
$type->getGenericType(),
$type,
);
}

return $type->getGenericType();
}
if ($allowString) {
return TypeCombinator::union(
new ObjectWithoutClassType(),
new ClassStringType(),
);
}

return new ObjectWithoutClassType();
},
);
}

}
48 changes: 7 additions & 41 deletions src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php
Expand Up @@ -9,18 +9,9 @@
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\ClassStringType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;
use function count;
use function strtolower;

Expand All @@ -29,6 +20,12 @@ class IsSubclassOfFunctionTypeSpecifyingExtension implements FunctionTypeSpecify

private TypeSpecifier $typeSpecifier;

public function __construct(
private IsAFunctionTypeSpecifyingHelper $isAFunctionTypeSpecifyingHelper,
)
{
}

public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool
{
return strtolower($functionReflection->getName()) === 'is_subclass_of'
Expand All @@ -48,40 +45,9 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
return new SpecifiedTypes([], []);
}

$type = TypeTraverser::map($classType, static function (Type $type, callable $traverse) use ($allowString): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}
if ($type instanceof ConstantStringType) {
if ($allowString) {
return TypeCombinator::union(
new ObjectType($type->getValue()),
new GenericClassStringType(new ObjectType($type->getValue())),
);
}
return new ObjectType($type->getValue());
}
if ($type instanceof GenericClassStringType) {
if ($allowString) {
return TypeCombinator::union(
$type->getGenericType(),
$type,
);
}
return $type->getGenericType();
}
if ($allowString) {
return TypeCombinator::union(
new ObjectWithoutClassType(),
new ClassStringType(),
);
}
return new ObjectWithoutClassType();
});

return $this->typeSpecifier->create(
$node->getArgs()[0]->value,
$type,
$this->isAFunctionTypeSpecifyingHelper->determineType($classType, $allowString),
$context,
false,
$scope,
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -726,6 +726,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/countable.php');

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6696.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6704.php');
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/filter-iterator-child-class.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/smaller-than-benevolent.php');

Expand Down
12 changes: 6 additions & 6 deletions tests/PHPStan/Analyser/TypeSpecifierTest.php
Expand Up @@ -445,7 +445,7 @@ public function dataCondition(): array
)),
]),
['$foo' => 'static(DateTime)'],
['$foo' => '~static(DateTime)'],
[],
],
[
new FuncCall(new Name('is_a'), [
Expand All @@ -461,7 +461,7 @@ public function dataCondition(): array
new Arg(new Variable('genericClassString')),
]),
['$foo' => 'Bar'],
['$foo' => '~Bar'],
[],
],
[
new FuncCall(new Name('is_a'), [
Expand All @@ -470,15 +470,15 @@ public function dataCondition(): array
new Arg(new Expr\ConstFetch(new Name('true'))),
]),
['$foo' => 'class-string<Foo>|Foo'],
['$foo' => '~Foo'],
['$foo' => '~class-string<Foo>|Foo'],
],
[
new FuncCall(new Name('is_a'), [
new Arg(new Variable('foo')),
new Arg(new Variable('className')),
new Arg(new Expr\ConstFetch(new Name('true'))),
]),
['$foo' => 'class-string<object>|object'],
['$foo' => 'class-string|object'],
[],
],
[
Expand All @@ -488,15 +488,15 @@ public function dataCondition(): array
new Arg(new Variable('unknown')),
]),
['$foo' => 'class-string<Foo>|Foo'],
['$foo' => '~Foo'],
['$foo' => '~class-string<Foo>|Foo'],
],
[
new FuncCall(new Name('is_a'), [
new Arg(new Variable('foo')),
new Arg(new Variable('className')),
new Arg(new Variable('unknown')),
]),
['$foo' => 'class-string<object>|object'],
['$foo' => 'class-string|object'],
[],
],
[
Expand Down
22 changes: 22 additions & 0 deletions tests/PHPStan/Analyser/data/bug-6704.php
@@ -0,0 +1,22 @@
<?php declare(strict_types = 1);

namespace Bug6704;

use DateTimeImmutable;
use stdClass;
use function PHPStan\Testing\assertType;

/**
* @param class-string<DateTimeImmutable>|class-string<stdClass> $a
* @param DateTimeImmutable|stdClass $b
*/
function foo(string $a, object $b): void
{
if (!is_a($a, stdClass::class, true)) {
assertType('class-string<DateTimeImmutable>', $a);
}

if (!is_a($b, stdClass::class)) {
assertType('DateTimeImmutable', $b);
}
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/data/is-a.php
Expand Up @@ -23,6 +23,6 @@ function (string $foo) {

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