Skip to content

Commit 1441481

Browse files
committed
QueryBuilder - analyse branches combinations
1 parent 45b9a89 commit 1441481

14 files changed

+380
-80
lines changed

phpstan.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ parameters:
1212
ignoreErrors:
1313
- '~^Parameter \#1 \$node \(.*\) of method .*Rule::processNode\(\) should be contravariant with parameter \$node \(PhpParser\\Node\) of method PHPStan\\Rules\\Rule::processNode\(\)$~'
1414
-
15-
message: '~^Variable method call on Doctrine\\ORM\\QueryBuilder\.$~'
15+
message: '~^Variable method call on Doctrine\\ORM\\QueryBuilder~'
1616
path: */src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php
1717
-
1818
message: '~^Variable method call on Doctrine\\ORM\\Query\\Expr\.$~'

src/Rules/Doctrine/ORM/DqlRule.php

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Rules\Rule;
88
use PHPStan\ShouldNotHappenException;
9-
use PHPStan\Type\Constant\ConstantStringType;
109
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
1110
use PHPStan\Type\ObjectType;
11+
use PHPStan\Type\TypeUtils;
1212

1313
class DqlRule implements Rule
1414
{
@@ -41,11 +41,6 @@ public function processNode(Node $node, Scope $scope): array
4141
return [];
4242
}
4343

44-
$dqlType = $scope->getType($node->args[0]->value);
45-
if (!$dqlType instanceof ConstantStringType) {
46-
return [];
47-
}
48-
4944
$methodName = $node->name->toLowerString();
5045
if ($methodName !== 'createquery') {
5146
return [];
@@ -57,6 +52,11 @@ public function processNode(Node $node, Scope $scope): array
5752
return [];
5853
}
5954

55+
$dqls = TypeUtils::getConstantStrings($scope->getType($node->args[0]->value));
56+
if (count($dqls) === 0) {
57+
return [];
58+
}
59+
6060
$objectManager = $this->objectMetadataResolver->getObjectManager();
6161
if ($objectManager === null) {
6262
throw new ShouldNotHappenException('Please provide the "objectManagerLoader" setting for the DQL validation.');
@@ -68,16 +68,17 @@ public function processNode(Node $node, Scope $scope): array
6868
/** @var \Doctrine\ORM\EntityManagerInterface $objectManager */
6969
$objectManager = $objectManager;
7070

71-
$dql = $dqlType->getValue();
72-
$query = $objectManager->createQuery($dql);
73-
74-
try {
75-
$query->getSQL();
76-
} catch (\Doctrine\ORM\Query\QueryException $e) {
77-
return [sprintf('DQL: %s', $e->getMessage())];
71+
$messages = [];
72+
foreach ($dqls as $dql) {
73+
$query = $objectManager->createQuery($dql->getValue());
74+
try {
75+
$query->getSQL();
76+
} catch (\Doctrine\ORM\Query\QueryException $e) {
77+
$messages[] = sprintf('DQL: %s', $e->getMessage());
78+
}
7879
}
7980

80-
return [];
81+
return $messages;
8182
}
8283

8384
}

src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
use PHPStan\Analyser\Scope;
88
use PHPStan\Rules\Rule;
99
use PHPStan\ShouldNotHappenException;
10-
use PHPStan\Type\Constant\ConstantStringType;
10+
use PHPStan\Type\Doctrine\DoctrineTypeUtils;
1111
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
12-
use PHPStan\Type\Doctrine\QueryBuilder\QueryBuilderType;
1312
use PHPStan\Type\ObjectType;
13+
use PHPStan\Type\TypeUtils;
1414

1515
class QueryBuilderDqlRule implements Rule
1616
{
@@ -51,7 +51,8 @@ public function processNode(Node $node, Scope $scope): array
5151
}
5252

5353
$calledOnType = $scope->getType($node->var);
54-
if (!$calledOnType instanceof QueryBuilderType) {
54+
$queryBuilderTypes = DoctrineTypeUtils::getQueryBuilderTypes($calledOnType);
55+
if (count($queryBuilderTypes) === 0) {
5556
if (
5657
$this->reportDynamicQueryBuilders
5758
&& (new ObjectType('Doctrine\ORM\QueryBuilder'))->isSuperTypeOf($calledOnType)->yes()
@@ -69,7 +70,8 @@ public function processNode(Node $node, Scope $scope): array
6970
return [sprintf('Internal error: %s', $e->getMessage())];
7071
}
7172

72-
if (!$dqlType instanceof ConstantStringType) {
73+
$dqls = TypeUtils::getConstantStrings($dqlType);
74+
if (count($dqls) === 0) {
7375
if ($this->reportDynamicQueryBuilders) {
7476
return [
7577
'Could not analyse QueryBuilder with dynamic arguments.',
@@ -91,18 +93,21 @@ public function processNode(Node $node, Scope $scope): array
9193
/** @var \Doctrine\ORM\EntityManagerInterface $objectManager */
9294
$objectManager = $objectManager;
9395

94-
try {
95-
$objectManager->createQuery($dqlType->getValue())->getSQL();
96-
} catch (\Doctrine\ORM\Query\QueryException $e) {
97-
$message = sprintf('QueryBuilder: %s', $e->getMessage());
98-
if (strpos($e->getMessage(), '[Syntax Error]') === 0) {
99-
$message .= sprintf("\nDQL: %s", $dqlType->getValue());
96+
$messages = [];
97+
foreach ($dqls as $dql) {
98+
try {
99+
$objectManager->createQuery($dql->getValue())->getSQL();
100+
} catch (\Doctrine\ORM\Query\QueryException $e) {
101+
$message = sprintf('QueryBuilder: %s', $e->getMessage());
102+
if (strpos($e->getMessage(), '[Syntax Error]') === 0) {
103+
$message .= sprintf("\nDQL: %s", $dql->getValue());
104+
}
105+
106+
$messages[] = $message;
100107
}
101-
102-
return [$message];
103108
}
104109

105-
return [];
110+
return $messages;
106111
}
107112

108113
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine;
4+
5+
use PHPStan\Type\Doctrine\Query\QueryType;
6+
use PHPStan\Type\Doctrine\QueryBuilder\QueryBuilderType;
7+
use PHPStan\Type\Type;
8+
use PHPStan\Type\UnionType;
9+
10+
class DoctrineTypeUtils
11+
{
12+
13+
/**
14+
* @param \PHPStan\Type\Type $type
15+
* @return \PHPStan\Type\Doctrine\QueryBuilder\QueryBuilderType[]
16+
*/
17+
public static function getQueryBuilderTypes(Type $type): array
18+
{
19+
if ($type instanceof QueryBuilderType) {
20+
return [$type];
21+
}
22+
23+
if ($type instanceof UnionType) {
24+
$types = [];
25+
foreach ($type->getTypes() as $innerType) {
26+
if (!$innerType instanceof QueryBuilderType) {
27+
return [];
28+
}
29+
30+
$types[] = $innerType;
31+
}
32+
33+
return $types;
34+
}
35+
36+
return [];
37+
}
38+
39+
/**
40+
* @param \PHPStan\Type\Type $type
41+
* @return \PHPStan\Type\Doctrine\Query\QueryType[]
42+
*/
43+
public static function getQueryTypes(Type $type): array
44+
{
45+
if ($type instanceof QueryType) {
46+
return [$type];
47+
}
48+
49+
if ($type instanceof UnionType) {
50+
$types = [];
51+
foreach ($type->getTypes() as $innerType) {
52+
if (!$innerType instanceof QueryType) {
53+
return [];
54+
}
55+
56+
$types[] = $innerType;
57+
}
58+
59+
return $types;
60+
}
61+
62+
return [];
63+
}
64+
65+
}

src/Type/Doctrine/Query/QueryGetDqlDynamicReturnTypeExtension.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
use PHPStan\Reflection\MethodReflection;
88
use PHPStan\Reflection\ParametersAcceptorSelector;
99
use PHPStan\Type\Constant\ConstantStringType;
10+
use PHPStan\Type\Doctrine\DoctrineTypeUtils;
1011
use PHPStan\Type\DynamicMethodReturnTypeExtension;
1112
use PHPStan\Type\Type;
13+
use PHPStan\Type\TypeCombinator;
1214

1315
class QueryGetDqlDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
1416
{
@@ -30,15 +32,21 @@ public function getTypeFromMethodCall(
3032
): Type
3133
{
3234
$calledOnType = $scope->getType($methodCall->var);
33-
if (!$calledOnType instanceof QueryType) {
35+
$queryTypes = DoctrineTypeUtils::getQueryTypes($calledOnType);
36+
if (count($queryTypes) === 0) {
3437
return ParametersAcceptorSelector::selectFromArgs(
3538
$scope,
3639
$methodCall->args,
3740
$methodReflection->getVariants()
3841
)->getReturnType();
3942
}
4043

41-
return new ConstantStringType($calledOnType->getDql());
44+
$dqls = [];
45+
foreach ($queryTypes as $queryType) {
46+
$dqls[] = new ConstantStringType($queryType->getDql());
47+
}
48+
49+
return TypeCombinator::union(...$dqls);
4250
}
4351

4452
}

src/Type/Doctrine/Query/QueryType.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
namespace PHPStan\Type\Doctrine\Query;
44

5+
use PHPStan\TrinaryLogic;
56
use PHPStan\Type\ObjectType;
7+
use PHPStan\Type\Type;
68

79
class QueryType extends ObjectType
810
{
@@ -16,6 +18,24 @@ public function __construct(string $dql)
1618
$this->dql = $dql;
1719
}
1820

21+
public function equals(Type $type): bool
22+
{
23+
if ($type instanceof self) {
24+
return $this->getDql() === $type->getDql();
25+
}
26+
27+
return parent::equals($type);
28+
}
29+
30+
public function isSuperTypeOf(Type $type): TrinaryLogic
31+
{
32+
if ($type instanceof self) {
33+
return TrinaryLogic::createFromBoolean($this->equals($type));
34+
}
35+
36+
return parent::isSuperTypeOf($type);
37+
}
38+
1939
public function getDql(): string
2040
{
2141
return $this->dql;

src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
use PHPStan\Reflection\ParametersAcceptorSelector;
1010
use PHPStan\Rules\Doctrine\ORM\DynamicQueryBuilderArgumentException;
1111
use PHPStan\Type\Doctrine\ArgumentsProcessor;
12+
use PHPStan\Type\Doctrine\DoctrineTypeUtils;
1213
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
1314
use PHPStan\Type\Doctrine\Query\QueryType;
1415
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeCombinator;
1517
use function in_array;
1618
use function method_exists;
1719
use function strtolower;
@@ -61,7 +63,8 @@ public function getTypeFromMethodCall(
6163
$methodCall->args,
6264
$methodReflection->getVariants()
6365
)->getReturnType();
64-
if (!$calledOnType instanceof QueryBuilderType) {
66+
$queryBuilderTypes = DoctrineTypeUtils::getQueryBuilderTypes($calledOnType);
67+
if (count($queryBuilderTypes) === 0) {
6568
return $defaultReturnType;
6669
}
6770

@@ -77,47 +80,52 @@ public function getTypeFromMethodCall(
7780
/** @var \Doctrine\ORM\EntityManagerInterface $objectManager */
7881
$objectManager = $objectManager;
7982

80-
$queryBuilder = $objectManager->createQueryBuilder();
81-
82-
foreach ($calledOnType->getMethodCalls() as $calledMethodCall) {
83-
if (!$calledMethodCall->name instanceof Identifier) {
84-
continue;
85-
}
86-
87-
$methodName = $calledMethodCall->name->toString();
88-
$lowerMethodName = strtolower($methodName);
89-
if (in_array($lowerMethodName, [
90-
'setparameter',
91-
'setparameters',
92-
], true)) {
93-
continue;
94-
}
95-
96-
if ($lowerMethodName === 'setfirstresult') {
97-
$queryBuilder->setFirstResult(0);
98-
continue;
99-
}
100-
101-
if ($lowerMethodName === 'setmaxresults') {
102-
$queryBuilder->setMaxResults(10);
103-
continue;
104-
}
105-
106-
if (!method_exists($queryBuilder, $methodName)) {
107-
continue;
108-
}
109-
110-
try {
111-
$args = $this->argumentsProcessor->processArgs($scope, $methodName, $calledMethodCall->args);
112-
} catch (DynamicQueryBuilderArgumentException $e) {
113-
// todo parameter "detectDynamicQueryBuilders" a hlasit jako error - pro oddebugovani
114-
return $defaultReturnType;
83+
$resultTypes = [];
84+
foreach ($queryBuilderTypes as $queryBuilderType) {
85+
$queryBuilder = $objectManager->createQueryBuilder();
86+
87+
foreach ($queryBuilderType->getMethodCalls() as $calledMethodCall) {
88+
if (!$calledMethodCall->name instanceof Identifier) {
89+
continue;
90+
}
91+
92+
$methodName = $calledMethodCall->name->toString();
93+
$lowerMethodName = strtolower($methodName);
94+
if (in_array($lowerMethodName, [
95+
'setparameter',
96+
'setparameters',
97+
], true)) {
98+
continue;
99+
}
100+
101+
if ($lowerMethodName === 'setfirstresult') {
102+
$queryBuilder->setFirstResult(0);
103+
continue;
104+
}
105+
106+
if ($lowerMethodName === 'setmaxresults') {
107+
$queryBuilder->setMaxResults(10);
108+
continue;
109+
}
110+
111+
if (!method_exists($queryBuilder, $methodName)) {
112+
continue;
113+
}
114+
115+
try {
116+
$args = $this->argumentsProcessor->processArgs($scope, $methodName, $calledMethodCall->args);
117+
} catch (DynamicQueryBuilderArgumentException $e) {
118+
// todo parameter "detectDynamicQueryBuilders" a hlasit jako error - pro oddebugovani
119+
return $defaultReturnType;
120+
}
121+
122+
$queryBuilder->{$methodName}(...$args);
115123
}
116124

117-
$queryBuilder->{$methodName}(...$args);
125+
$resultTypes[] = new QueryType($queryBuilder->getDQL());
118126
}
119127

120-
return new QueryType($queryBuilder->getDQL());
128+
return TypeCombinator::union(...$resultTypes);
121129
}
122130

123131
}

0 commit comments

Comments
 (0)