Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit fdba25e

Browse files
ruudkfluffycondor
andcommitted
Narrow type of Collection::first() when using Collection::isEmpty()
Co-authored-by: Semyon <7ionmail@gmail.com>
1 parent f2a650c commit fdba25e

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)

AltStyle によって変換されたページ (->オリジナル) /