Skip to content

Commit

Permalink
Narrow type of Collection::first() when using Collection::isEmpty()
Browse files Browse the repository at this point in the history
Co-authored-by: Semyon <[email protected]>
  • Loading branch information
ruudk and fluffycondor committed Feb 9, 2021
1 parent f2a650c commit 9ce54d8
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 0 deletions.
6 changes: 6 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,9 @@ services:
tags: [phpstan.doctrine.typeDescriptor]
arguments:
uuidTypeName: Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType

# Doctrine Collection
-
class: PHPStan\Type\Doctrine\Collection\FirstTypeSpecifyingExtension
tags:
- phpstan.typeSpecifier.methodTypeSpecifyingExtension
62 changes: 62 additions & 0 deletions src/Type/Doctrine/Collection/FirstTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Doctrine\Collection;

use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\MethodTypeSpecifyingExtension;

final class FirstTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
{

private const COLLECTION_CLASS = 'Doctrine\Common\Collections\Collection';
private const IS_EMPTY_METHOD_NAME = 'isEmpty';
private const FIRST_METHOD_NAME = 'first';

/** @var TypeSpecifier */
private $typeSpecifier;

public function getClass(): string
{
return self::COLLECTION_CLASS;
}

public function isMethodSupported(
MethodReflection $methodReflection,
MethodCall $node,
TypeSpecifierContext $context
): bool
{
return (
$methodReflection->getDeclaringClass()->getName() === self::COLLECTION_CLASS
|| $methodReflection->getDeclaringClass()->isSubclassOf(self::COLLECTION_CLASS)
)
&& $methodReflection->getName() === self::IS_EMPTY_METHOD_NAME;
}

public function specifyTypes(
MethodReflection $methodReflection,
MethodCall $node,
Scope $scope,
TypeSpecifierContext $context
): SpecifiedTypes
{
return $this->typeSpecifier->create(
new MethodCall($node->var, self::FIRST_METHOD_NAME),
new ConstantBooleanType(false),
$context
);
}

public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
{
$this->typeSpecifier = $typeSpecifier;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Doctrine\Collection;

use PHPStan\Rules\Rule;

/**
* @extends \PHPStan\Testing\RuleTestCase<VariableTypeReportingRule>
*/
class FirstTypeSpecifyingExtensionTest extends \PHPStan\Testing\RuleTestCase
{

protected function getRule(): Rule
{
return new VariableTypeReportingRule();
}

/**
* @return \PHPStan\Type\MethodTypeSpecifyingExtension[]
*/
protected function getMethodTypeSpecifyingExtensions(): array
{
return [
new FirstTypeSpecifyingExtension(),
];
}

public function testExtension(): void
{
$this->analyse([__DIR__ . '/data/collection.php'], [
[
'Variable $entityOrFalse is: MyEntity|false',
18,
],
[
'Variable $false is: false',
22,
],
[
'Variable $entity is: MyEntity',
27,
],
]);
}

}
41 changes: 41 additions & 0 deletions tests/Type/Doctrine/Collection/VariableTypeReportingRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Doctrine\Collection;

use PhpParser\Node;
use PHPStan\Analyser\Scope;

/**
* @implements \PHPStan\Rules\Rule<Node\Expr\Variable>
*/
class VariableTypeReportingRule implements \PHPStan\Rules\Rule
{

public function getNodeType(): string
{
return Node\Expr\Variable::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!is_string($node->name)) {
return [];
}
if (!$scope->isInFirstLevelStatement()) {
return [];
};

if ($scope->isInExpressionAssign($node)) {
return [];
}

return [
sprintf(
'Variable $%s is: %s',
$node->name,
$scope->getType($node)->describe(\PHPStan\Type\VerbosityLevel::value())
),
];
}

}
28 changes: 28 additions & 0 deletions tests/Type/Doctrine/Collection/data/collection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);

use Doctrine\Common\Collections\ArrayCollection;

class MyEntity
{

}

$new = new MyEntity();

/**
* @var ArrayCollection<int, MyEntity> $collection
*/
$collection = new ArrayCollection();

$entityOrFalse = $collection->first();
$entityOrFalse;

if ($collection->isEmpty()) {
$false = $collection->first();
$false;
}

if (!$collection->isEmpty()) {
$entity = $collection->first();
$entity;
}

0 comments on commit 9ce54d8

Please sign in to comment.