Skip to content

Commit 324d3cd

Browse files
committed
Merge remote-tracking branch 'origin/1.3.x' into 1.4.x
2 parents 39d78ad + b0e0c32 commit 324d3cd

14 files changed

+199
-31
lines changed

extension.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,3 +398,8 @@ services:
398398
class: PHPStan\PhpDoc\Doctrine\QueryTypeNodeResolverExtension
399399
tags:
400400
- phpstan.phpDoc.typeNodeResolverExtension
401+
402+
-
403+
class: PHPStan\Type\Doctrine\EntityManagerInterfaceThrowTypeExtension
404+
tags:
405+
- phpstan.dynamicMethodThrowTypeExtension

phpstan-baseline.neon

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ parameters:
4040
count: 1
4141
path: tests/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php
4242

43+
-
44+
message: "#^Accessing PHPStan\\\\Rules\\\\Exceptions\\\\TooWideMethodThrowTypeRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#"
45+
count: 1
46+
path: tests/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php
47+
4348
-
4449
message: "#^Accessing PHPStan\\\\Rules\\\\DeadCode\\\\UnusedPrivatePropertyRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#"
4550
count: 1
@@ -59,4 +64,3 @@ parameters:
5964
message: "#^Accessing PHPStan\\\\Rules\\\\Properties\\\\MissingReadOnlyPropertyAssignRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#"
6065
count: 1
6166
path: tests/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php
62-

rules.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ services:
3535
class: PHPStan\Rules\Doctrine\ORM\QueryBuilderDqlRule
3636
arguments:
3737
reportDynamicQueryBuilders: %doctrine.reportDynamicQueryBuilders%
38+
searchOtherMethodsForQueryBuilderBeginning: %doctrine.searchOtherMethodsForQueryBuilderBeginning%
3839
tags:
3940
- phpstan.rules.rule
4041
-

src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHPStan\Rules\RuleErrorBuilder;
1313
use PHPStan\Type\Doctrine\DoctrineTypeUtils;
1414
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
15+
use PHPStan\Type\Doctrine\QueryBuilder\OtherMethodQueryBuilderParser;
1516
use PHPStan\Type\ObjectType;
1617
use PHPStan\Type\TypeUtils;
1718
use Throwable;
@@ -31,13 +32,23 @@ class QueryBuilderDqlRule implements Rule
3132
/** @var bool */
3233
private $reportDynamicQueryBuilders;
3334

35+
/** @var OtherMethodQueryBuilderParser */
36+
private $otherMethodQueryBuilderParser;
37+
38+
/** @var bool */
39+
private $searchOtherMethodsForQueryBuilderBeginning;
40+
3441
public function __construct(
3542
ObjectMetadataResolver $objectMetadataResolver,
36-
bool $reportDynamicQueryBuilders
43+
OtherMethodQueryBuilderParser $otherMethodQueryBuilderParser,
44+
bool $reportDynamicQueryBuilders,
45+
bool $searchOtherMethodsForQueryBuilderBeginning
3746
)
3847
{
3948
$this->objectMetadataResolver = $objectMetadataResolver;
49+
$this->otherMethodQueryBuilderParser = $otherMethodQueryBuilderParser;
4050
$this->reportDynamicQueryBuilders = $reportDynamicQueryBuilders;
51+
$this->searchOtherMethodsForQueryBuilderBeginning = $searchOtherMethodsForQueryBuilderBeginning;
4152
}
4253

4354
public function getNodeType(): string
@@ -58,6 +69,14 @@ public function processNode(Node $node, Scope $scope): array
5869
$calledOnType = $scope->getType($node->var);
5970
$queryBuilderTypes = DoctrineTypeUtils::getQueryBuilderTypes($calledOnType);
6071
if (count($queryBuilderTypes) === 0) {
72+
73+
if ($this->searchOtherMethodsForQueryBuilderBeginning) {
74+
$queryBuilderTypes = $this->otherMethodQueryBuilderParser->getQueryBuilderTypes($scope, $node);
75+
if (count($queryBuilderTypes) !== 0) {
76+
return [];
77+
}
78+
}
79+
6180
if (
6281
$this->reportDynamicQueryBuilders
6382
&& (new ObjectType('Doctrine\ORM\QueryBuilder'))->isSuperTypeOf($calledOnType)->yes()
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine;
4+
5+
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
6+
use Doctrine\ORM\EntityManagerInterface;
7+
use Doctrine\ORM\Exception\ORMException;
8+
use Doctrine\Persistence\ObjectManager;
9+
use PhpParser\Node\Expr\MethodCall;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Reflection\MethodReflection;
12+
use PHPStan\Type\DynamicMethodThrowTypeExtension;
13+
use PHPStan\Type\ObjectType;
14+
use PHPStan\Type\Type;
15+
use PHPStan\Type\TypeCombinator;
16+
use function array_map;
17+
18+
class EntityManagerInterfaceThrowTypeExtension implements DynamicMethodThrowTypeExtension
19+
{
20+
21+
public const SUPPORTED_METHOD = [
22+
'flush' => [
23+
ORMException::class,
24+
UniqueConstraintViolationException::class,
25+
],
26+
];
27+
28+
public function isMethodSupported(MethodReflection $methodReflection): bool
29+
{
30+
return $methodReflection->getDeclaringClass()->getName() === ObjectManager::class
31+
&& isset(self::SUPPORTED_METHOD[$methodReflection->getName()]);
32+
}
33+
34+
public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
35+
{
36+
$type = $scope->getType($methodCall->var);
37+
38+
if ((new ObjectType(EntityManagerInterface::class))->isSuperTypeOf($type)->yes()) {
39+
return TypeCombinator::union(
40+
...array_map(static function ($class): Type {
41+
return new ObjectType($class);
42+
}, self::SUPPORTED_METHOD[$methodReflection->getName()])
43+
);
44+
}
45+
46+
return $methodReflection->getThrowType();
47+
}
48+
49+
}

src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
use PHPStan\Parser\Parser;
2020
use PHPStan\Reflection\ReflectionProvider;
2121
use PHPStan\Type\Generic\TemplateTypeMap;
22+
use PHPStan\Type\IntersectionType;
23+
use PHPStan\Type\Type;
24+
use PHPStan\Type\TypeTraverser;
25+
use PHPStan\Type\UnionType;
2226
use function count;
2327
use function is_array;
2428

@@ -118,11 +122,18 @@ private function findQueryBuilderTypesInCalledMethod(Scope $scope, MethodCall $m
118122
}
119123

120124
$exprType = $scope->getType($node->expr);
121-
if (!$exprType instanceof QueryBuilderType) {
122-
return;
123-
}
124125

125-
$queryBuilderTypes[] = $exprType;
126+
TypeTraverser::map($exprType, static function (Type $type, callable $traverse) use (&$queryBuilderTypes): Type {
127+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
128+
return $traverse($type);
129+
}
130+
131+
if ($type instanceof QueryBuilderType) {
132+
$queryBuilderTypes[] = $type;
133+
}
134+
135+
return $type;
136+
});
126137
});
127138

128139
return $queryBuilderTypes;

stubs/EntityManagerInterface.stub

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,12 @@ interface EntityManagerInterface extends ObjectManager
6060
*/
6161
public function copy($entity, $deep = false);
6262

63-
/**
64-
* @template T of object
65-
* @phpstan-param class-string<T> $className
66-
*
67-
* @phpstan-return ClassMetadata<T>
68-
*/
69-
public function getClassMetadata($className);
63+
/**
64+
* @template T of object
65+
* @phpstan-param class-string<T> $className
66+
*
67+
* @phpstan-return ClassMetadata<T>
68+
*/
69+
public function getClassMetadata($className);
70+
7071
}

stubs/bleedingEdge/ORM/QueryBuilder.stub

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ class QueryBuilder
5858
}
5959

6060
/**
61-
* @param literal-string $from
62-
* @param literal-string $alias
63-
* @param literal-string|null $indexBy
61+
* @param literal-string|class-string $from
62+
* @param literal-string $alias
63+
* @param literal-string|null $indexBy
6464
*
6565
* @return $this
6666
*/
@@ -70,7 +70,7 @@ class QueryBuilder
7070
}
7171

7272
/**
73-
* @param literal-string $join
73+
* @param literal-string|class-string $join
7474
* @param literal-string $alias
7575
* @param Expr\Join::ON|Expr\Join::WITH|null $conditionType
7676
* @param literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition
@@ -84,7 +84,7 @@ class QueryBuilder
8484
}
8585

8686
/**
87-
* @param literal-string $join
87+
* @param literal-string|class-string $join
8888
* @param literal-string $alias
8989
* @param Expr\Join::ON|Expr\Join::WITH|null $conditionType
9090
* @param literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition
@@ -98,7 +98,7 @@ class QueryBuilder
9898
}
9999

100100
/**
101-
* @param literal-string $join
101+
* @param literal-string|class-string $join
102102
* @param literal-string $alias
103103
* @param Expr\Join::ON|Expr\Join::WITH|null $conditionType
104104
* @param literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition

tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPStan\Rules\Rule;
66
use PHPStan\Testing\RuleTestCase;
77
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
8+
use PHPStan\Type\Doctrine\QueryBuilder\OtherMethodQueryBuilderParser;
89

910
/**
1011
* @extends RuleTestCase<QueryBuilderDqlRule>
@@ -14,7 +15,12 @@ class QueryBuilderDqlRuleSlowTest extends RuleTestCase
1415

1516
protected function getRule(): Rule
1617
{
17-
return new QueryBuilderDqlRule(new ObjectMetadataResolver(__DIR__ . '/entity-manager.php'), true);
18+
return new QueryBuilderDqlRule(
19+
new ObjectMetadataResolver(__DIR__ . '/entity-manager.php'),
20+
self::getContainer()->getByType(OtherMethodQueryBuilderParser::class),
21+
true,
22+
true
23+
);
1824
}
1925

2026
public function testRule(): void
@@ -40,10 +46,6 @@ public function testRule(): void
4046
'QueryBuilder: [Semantical Error] line 0, col 14 near \'Foo e\': Error: Class \'Foo\' is not defined.',
4147
71,
4248
],
43-
[
44-
'Could not analyse QueryBuilder with unknown beginning.',
45-
89,
46-
],
4749
[
4850
'Could not analyse QueryBuilder with dynamic arguments.',
4951
99,

tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPStan\Rules\Rule;
66
use PHPStan\Testing\RuleTestCase;
77
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
8+
use PHPStan\Type\Doctrine\QueryBuilder\OtherMethodQueryBuilderParser;
89

910
/**
1011
* @extends RuleTestCase<QueryBuilderDqlRule>
@@ -14,7 +15,12 @@ class QueryBuilderDqlRuleTest extends RuleTestCase
1415

1516
protected function getRule(): Rule
1617
{
17-
return new QueryBuilderDqlRule(new ObjectMetadataResolver(__DIR__ . '/entity-manager.php'), true);
18+
return new QueryBuilderDqlRule(
19+
new ObjectMetadataResolver(__DIR__ . '/entity-manager.php'),
20+
self::getContainer()->getByType(OtherMethodQueryBuilderParser::class),
21+
true,
22+
true
23+
);
1824
}
1925

2026
public function testRule(): void
@@ -40,10 +46,6 @@ public function testRule(): void
4046
'QueryBuilder: [Semantical Error] line 0, col 14 near \'Foo e\': Error: Class \'Foo\' is not defined.',
4147
71,
4248
],
43-
[
44-
'Could not analyse QueryBuilder with unknown beginning.',
45-
89,
46-
],
4749
[
4850
'Could not analyse QueryBuilder with dynamic arguments.',
4951
99,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Exceptions;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<TooWideMethodThrowTypeRule>
10+
*/
11+
class TooWideMethodThrowTypeRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return self::getContainer()->getByType(TooWideMethodThrowTypeRule::class);
17+
}
18+
19+
public function testRule(): void
20+
{
21+
$this->analyse([__DIR__ . '/data/entity-manager-interface.php'], []);
22+
}
23+
24+
public static function getAdditionalConfigFiles(): array
25+
{
26+
return [
27+
__DIR__ . '/../../../extension.neon',
28+
];
29+
}
30+
31+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace EntityManagerInterfaceThrowTypeExtensionTest;
4+
5+
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
6+
use Doctrine\ORM\EntityManagerInterface;
7+
use Doctrine\ORM\Exception\ORMException;
8+
9+
class Example
10+
{
11+
12+
/**
13+
* @throws ORMException
14+
* @throws UniqueConstraintViolationException
15+
*/
16+
public function doFoo(EntityManagerInterface $entityManager): void
17+
{
18+
$entityManager->flush();
19+
}
20+
21+
}

tests/Rules/Exceptions/data/unthrown-exception.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@ class FooFacade
88
/** @var \Doctrine\ORM\EntityManager */
99
private $entityManager;
1010

11+
/** @var \Doctrine\ORM\EntityManagerInterface */
12+
private $entityManagerInterface;
13+
1114
public function doFoo(): void
1215
{
1316
try {
1417
$this->entityManager->flush();
1518
} catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) {
1619
// pass
1720
}
21+
try {
22+
$this->entityManagerInterface->flush();
23+
} catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) {
24+
// pass
25+
}
1826
}
1927

2028
}

tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,11 @@ public function testQueryResultTypeIsVoidWithDeleteOrUpdate(EntityManagerInterfa
140140

141141
public function testQueryTypeIsInferredOnAcrossMethods(EntityManagerInterface $em): void
142142
{
143-
$query = $this->getQueryBuilder($em)
144-
->getQuery();
143+
$query = $this->getQueryBuilder($em)->getQuery();
144+
$branchingQuery = $this->getBranchingQueryBuilder($em)->getQuery();
145145

146146
assertType('Doctrine\ORM\Query<null, QueryResult\Entities\Many>', $query);
147+
assertType('Doctrine\ORM\Query<null, QueryResult\Entities\Many>', $branchingQuery);
147148
}
148149

149150
private function getQueryBuilder(EntityManagerInterface $em): QueryBuilder
@@ -152,4 +153,17 @@ private function getQueryBuilder(EntityManagerInterface $em): QueryBuilder
152153
->select('m')
153154
->from(Many::class, 'm');
154155
}
156+
157+
private function getBranchingQueryBuilder(EntityManagerInterface $em): QueryBuilder
158+
{
159+
$queryBuilder = $em->createQueryBuilder()
160+
->select('m')
161+
->from(Many::class, 'm');
162+
163+
if (random_int(0, 1) === 1) {
164+
$queryBuilder->andWhere('m.intColumn = 1');
165+
}
166+
167+
return $queryBuilder;
168+
}
155169
}

0 commit comments

Comments
 (0)