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
9 changes: 7 additions & 2 deletions src/Parser/TypeTraverserInstanceofVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ final class TypeTraverserInstanceofVisitor extends NodeVisitorAbstract

public const ATTRIBUTE_NAME = 'insideTypeTraverserMap';

private const TYPE_TRAVERSER_CLASSES = [
'phpstan\\type\\typetraverser',
'phpstan\\type\\simultaneoustypetraverser',
];

private int $depth = 0;

#[Override]
Expand All @@ -33,7 +38,7 @@ public function enterNode(Node $node): ?Node
if (
$node instanceof Node\Expr\StaticCall
&& $node->class instanceof Node\Name
&& $node->class->toLowerString() === 'phpstan\\type\\typetraverser'
&& \in_array($node->class->toLowerString(), self::TYPE_TRAVERSER_CLASSES, true)
&& $node->name instanceof Node\Identifier
&& $node->name->toLowerString() === 'map'
) {
Expand All @@ -49,7 +54,7 @@ public function leaveNode(Node $node): ?Node
if (
$node instanceof Node\Expr\StaticCall
&& $node->class instanceof Node\Name
&& $node->class->toLowerString() === 'phpstan\\type\\typetraverser'
&& \in_array($node->class->toLowerString(), self::TYPE_TRAVERSER_CLASSES, true)
&& $node->name instanceof Node\Identifier
&& $node->name->toLowerString() === 'map'
) {
Expand Down
26 changes: 18 additions & 8 deletions src/Rules/RuleLevelHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\CallableType;
Expand All @@ -16,6 +17,7 @@
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
use PHPStan\Type\SimultaneousTypeTraverser;
use PHPStan\Type\StrictMixedType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
Expand Down Expand Up @@ -87,15 +89,18 @@ private function transformCommonType(Type $type): Type
private function transformAcceptedType(Type $acceptingType, Type $acceptedType): array
{
$checkForUnion = $this->checkUnionTypes;
$acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type {
$acceptedType = SimultaneousTypeTraverser::map($acceptedType, $acceptingType, function (Type $acceptedType, Type $acceptingType, callable $traverse) use (&$checkForUnion): Type {
if ($acceptedType instanceof CallableType) {
if ($acceptedType->isCommonCallable()) {
return $acceptedType;
}
if (!$acceptingType instanceof ParametersAcceptor) {
return $acceptedType;
}

return new CallableType(
$acceptedType->getParameters(),
$traverse($this->transformCommonType($acceptedType->getReturnType())),
$traverse($this->transformCommonType($acceptedType->getReturnType()), $acceptingType->getReturnType()),
$acceptedType->isVariadic(),
$acceptedType->getTemplateTypeMap(),
$acceptedType->getResolvedTemplateTypeMap(),
Expand All @@ -109,9 +114,13 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType):
return $acceptedType;
}

if (!$acceptingType instanceof ParametersAcceptor) {
return $acceptedType;
}

return new ClosureType(
$acceptedType->getParameters(),
$traverse($this->transformCommonType($acceptedType->getReturnType())),
$traverse($this->transformCommonType($acceptedType->getReturnType()), $acceptingType->getReturnType()),
$acceptedType->isVariadic(),
$acceptedType->getTemplateTypeMap(),
$acceptedType->getResolvedTemplateTypeMap(),
Expand All @@ -128,21 +137,22 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType):

if (
!$this->checkNullables
&& !$acceptingType instanceof NullType
&& !$acceptedType instanceof NullType
&& !$acceptedType instanceof BenevolentUnionType
&& !$acceptedType instanceof NullType
&& TypeCombinator::containsNull($acceptedType)
&& !TypeCombinator::containsNull($acceptingType)
) {
return $traverse(TypeCombinator::removeNull($acceptedType));
return $traverse(TypeCombinator::removeNull($acceptedType), $acceptingType);
}

if ($this->checkBenevolentUnionTypes) {
if ($acceptedType instanceof BenevolentUnionType) {
$checkForUnion = true;
return $traverse(TypeUtils::toStrictUnion($acceptedType));
return $traverse(TypeUtils::toStrictUnion($acceptedType), $acceptingType);
}
}

return $traverse($this->transformCommonType($acceptedType));
return $traverse($this->transformCommonType($acceptedType), $acceptingType);
});

return [$acceptedType, $checkForUnion];
Expand Down
10 changes: 7 additions & 3 deletions src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -1260,13 +1260,17 @@ public function traverse(callable $cb): Type

public function traverseSimultaneously(Type $right, callable $cb): Type
{
if ($this->isArray()->yes() && $right->isArray()->yes()) {
if ($this->isArray()->yes() && !$right->isArray()->no()) {
$rightArray = $right->isArray()->maybe()
? TypeCombinator::intersect($right, new ArrayType(new MixedType(), new MixedType()))
: $right;

$changed = false;
$newTypes = [];

foreach ($this->types as $innerType) {
$newKeyType = $cb($innerType->getIterableKeyType(), $right->getIterableKeyType());
$newValueType = $cb($innerType->getIterableValueType(), $right->getIterableValueType());
$newKeyType = $cb($innerType->getIterableKeyType(), $rightArray->getIterableKeyType());
$newValueType = $cb($innerType->getIterableValueType(), $rightArray->getIterableValueType());
if ($newKeyType === $innerType->getIterableKeyType() && $newValueType === $innerType->getIterableValueType()) {
$newTypes[] = $innerType;
continue;
Expand Down
14 changes: 12 additions & 2 deletions src/Type/ObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -1690,11 +1690,21 @@ public function traverse(callable $cb): Type

public function traverseSimultaneously(Type $right, callable $cb): Type
{
if ($this->subtractedType === null) {
if (!$right instanceof SubtractableType) {
return $this;
}

$rightSubtractable = $right->getSubtractedType();
if ($this->subtractedType === null || $rightSubtractable === null) {
return $this;
}

return new self($this->className);
$newSubtractedType = $cb($this->subtractedType, $rightSubtractable);
if ($newSubtractedType !== $this->subtractedType) {
return new self($this->className, $newSubtractedType);
}

return $this;
}

public function getNakedClassReflection(): ?ClassReflection
Expand Down
14 changes: 12 additions & 2 deletions src/Type/ObjectWithoutClassType.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,21 @@ public function traverse(callable $cb): Type

public function traverseSimultaneously(Type $right, callable $cb): Type
{
if ($this->subtractedType === null) {
if (!$right instanceof SubtractableType) {
return $this;
}

return new self();
$rightSubtractable = $right->getSubtractedType();
if ($this->subtractedType === null || $rightSubtractable === null) {
return $this;
}

$newSubtractedType = $cb($this->subtractedType, $rightSubtractable);
if ($newSubtractedType !== $this->subtractedType) {
return new self($newSubtractedType);
}

return $this;
}

public function tryRemove(Type $typeToRemove): ?Type
Expand Down
14 changes: 12 additions & 2 deletions src/Type/StaticType.php
Original file line number Diff line number Diff line change
Expand Up @@ -745,11 +745,21 @@ public function traverse(callable $cb): Type

public function traverseSimultaneously(Type $right, callable $cb): Type
{
if ($this->subtractedType === null) {
if (!$right instanceof SubtractableType) {
return $this;
}

return new self($this->classReflection);
$rightSubtractable = $right->getSubtractedType();
if ($this->subtractedType === null || $rightSubtractable === null) {
return $this;
}

$newSubtractedType = $cb($this->subtractedType, $rightSubtractable);
if ($newSubtractedType !== $this->subtractedType) {
return new self($this->classReflection, $newSubtractedType);
}

return $this;
}

public function subtract(Type $type): Type
Expand Down
11 changes: 10 additions & 1 deletion tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
class ClosureReturnTypeRuleTest extends RuleTestCase
{

private bool $checkNullables = true;

protected function getRule(): Rule
{
return new ClosureReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true)));
return new ClosureReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper(self::createReflectionProvider(), $this->checkNullables, false, true, false, false, false, true)));
}

public function testClosureReturnTypeRule(): void
Expand Down Expand Up @@ -128,6 +130,13 @@ public function testBug7220(): void
$this->analyse([__DIR__ . '/data/bug-7220.php'], []);
}

public function testBug12008(): void
{
$this->checkNullables = false;

$this->analyse([__DIR__ . '/data/bug-12008.php'], []);
}

public function testBugFunctionMethodConstants(): void
{
$this->analyse([__DIR__ . '/data/bug-anonymous-function-method-constant.php'], []);
Expand Down
42 changes: 42 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-12008.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);

namespace Bug12008;

interface ProductOverview {
public function getId(): ?int;
}

/**
* @template T
*/
readonly class Pagination
{
/**
* @param iterable<T> $records
*/
public function __construct(
public iterable $records,
) {
}
}

class HelloWorld
{
private function respondToApiRequest(Closure|null $data): never {
exit;
}

/**
* @param list<ProductOverview> $products
*/
public function run(array $products): never {
$this->respondToApiRequest(function () use ($products) {
return new Pagination(array_map(
fn (ProductOverview $product) => [
'id' => $product->getId(),
],
$products,
));
});
}
}
Loading