Skip to content

Commit 2486f59

Browse files
ruudkfluffycondor
authored andcommitted
Narrow type of Collection::first() when using Collection::isEmpty()
Co-authored-by: Semyon <[email protected]>
1 parent f2a650c commit 2486f59

File tree

6 files changed

+184
-1
lines changed

6 files changed

+184
-1
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
},
1919
"require-dev": {
2020
"doctrine/annotations": "^1.11.0",
21-
"doctrine/collections": "^1.0",
21+
"doctrine/collections": "^1.6",
2222
"doctrine/common": "^2.7 || ^3.0",
2323
"doctrine/dbal": "^2.11.0",
2424
"doctrine/mongodb-odm": "^1.3 || ^2.1",

extension.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,9 @@ services:
310310
tags: [phpstan.doctrine.typeDescriptor]
311311
arguments:
312312
uuidTypeName: Ramsey\Uuid\Doctrine\UuidBinaryOrderedTimeType
313+
314+
# Doctrine Collection
315+
-
316+
class: PHPStan\Type\Doctrine\Collection\FirstTypeSpecifyingExtension
317+
tags:
318+
- phpstan.typeSpecifier.methodTypeSpecifyingExtension
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Collection;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Analyser\SpecifiedTypes;
8+
use PHPStan\Analyser\TypeSpecifier;
9+
use PHPStan\Analyser\TypeSpecifierAwareExtension;
10+
use PHPStan\Analyser\TypeSpecifierContext;
11+
use PHPStan\Reflection\MethodReflection;
12+
use PHPStan\Type\Constant\ConstantBooleanType;
13+
use PHPStan\Type\MethodTypeSpecifyingExtension;
14+
15+
final class FirstTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
16+
{
17+
18+
private const COLLECTION_CLASS = 'Doctrine\Common\Collections\Collection';
19+
private const IS_EMPTY_METHOD_NAME = 'isEmpty';
20+
private const FIRST_METHOD_NAME = 'first';
21+
22+
/** @var TypeSpecifier */
23+
private $typeSpecifier;
24+
25+
public function getClass(): string
26+
{
27+
return self::COLLECTION_CLASS;
28+
}
29+
30+
public function isMethodSupported(
31+
MethodReflection $methodReflection,
32+
MethodCall $node,
33+
TypeSpecifierContext $context
34+
): bool
35+
{
36+
return (
37+
$methodReflection->getDeclaringClass()->getName() === self::COLLECTION_CLASS
38+
|| $methodReflection->getDeclaringClass()->isSubclassOf(self::COLLECTION_CLASS)
39+
)
40+
&& $methodReflection->getName() === self::IS_EMPTY_METHOD_NAME;
41+
}
42+
43+
public function specifyTypes(
44+
MethodReflection $methodReflection,
45+
MethodCall $node,
46+
Scope $scope,
47+
TypeSpecifierContext $context
48+
): SpecifiedTypes
49+
{
50+
return $this->typeSpecifier->create(
51+
new MethodCall($node->var, self::FIRST_METHOD_NAME),
52+
new ConstantBooleanType(false),
53+
$context
54+
);
55+
}
56+
57+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
58+
{
59+
$this->typeSpecifier = $typeSpecifier;
60+
}
61+
62+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Collection;
4+
5+
use PHPStan\Rules\Rule;
6+
7+
/**
8+
* @extends \PHPStan\Testing\RuleTestCase<VariableTypeReportingRule>
9+
*/
10+
class FirstTypeSpecifyingExtensionTest extends \PHPStan\Testing\RuleTestCase
11+
{
12+
13+
protected function getRule(): Rule
14+
{
15+
return new VariableTypeReportingRule();
16+
}
17+
18+
/**
19+
* @return \PHPStan\Type\MethodTypeSpecifyingExtension[]
20+
*/
21+
protected function getMethodTypeSpecifyingExtensions(): array
22+
{
23+
return [
24+
new FirstTypeSpecifyingExtension(),
25+
];
26+
}
27+
28+
public function testExtension(): void
29+
{
30+
$this->analyse([__DIR__ . '/data/collection.php'], [
31+
[
32+
'Variable $entityOrFalse is: MyEntity|false',
33+
18,
34+
],
35+
[
36+
'Variable $false is: false',
37+
22,
38+
],
39+
[
40+
'Variable $entity is: MyEntity',
41+
27,
42+
],
43+
]);
44+
}
45+
46+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Collection;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
8+
/**
9+
* @implements \PHPStan\Rules\Rule<Node\Expr\Variable>
10+
*/
11+
class VariableTypeReportingRule implements \PHPStan\Rules\Rule
12+
{
13+
14+
public function getNodeType(): string
15+
{
16+
return Node\Expr\Variable::class;
17+
}
18+
19+
public function processNode(Node $node, Scope $scope): array
20+
{
21+
if (!is_string($node->name)) {
22+
return [];
23+
}
24+
if (!$scope->isInFirstLevelStatement()) {
25+
return [];
26+
};
27+
28+
if ($scope->isInExpressionAssign($node)) {
29+
return [];
30+
}
31+
32+
return [
33+
sprintf(
34+
'Variable $%s is: %s',
35+
$node->name,
36+
$scope->getType($node)->describe(\PHPStan\Type\VerbosityLevel::value())
37+
),
38+
];
39+
}
40+
41+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php declare(strict_types = 1);
2+
3+
use Doctrine\Common\Collections\ArrayCollection;
4+
5+
class MyEntity
6+
{
7+
8+
}
9+
10+
$new = new MyEntity();
11+
12+
/**
13+
* @var ArrayCollection<int, MyEntity> $collection
14+
*/
15+
$collection = new ArrayCollection();
16+
17+
$entityOrFalse = $collection->first();
18+
$entityOrFalse;
19+
20+
if ($collection->isEmpty()) {
21+
$false = $collection->first();
22+
$false;
23+
}
24+
25+
if (!$collection->isEmpty()) {
26+
$entity = $collection->first();
27+
$entity;
28+
}

0 commit comments

Comments
 (0)