Skip to content

Commit 3369068

Browse files
janedbalondrejmirtes
authored andcommitted
Proper aggregate function detection
1 parent 40ae315 commit 3369068

File tree

3 files changed

+342
-27
lines changed

3 files changed

+342
-27
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\Query;
4+
5+
use Doctrine\ORM\Query;
6+
use Doctrine\ORM\Query\AST;
7+
use function is_string;
8+
9+
class QueryAggregateFunctionDetectorTreeWalker extends Query\TreeWalkerAdapter
10+
{
11+
12+
public const HINT_HAS_AGGREGATE_FUNCTION = self::class . '::HINT_HAS_AGGREGATE_FUNCTION';
13+
14+
public function walkSelectStatement(AST\SelectStatement $selectStatement): void
15+
{
16+
$this->doWalkSelectClause($selectStatement->selectClause);
17+
}
18+
19+
/**
20+
* @param AST\SelectClause $selectClause
21+
*/
22+
public function doWalkSelectClause($selectClause): void
23+
{
24+
foreach ($selectClause->selectExpressions as $selectExpression) {
25+
$this->doWalkSelectExpression($selectExpression);
26+
}
27+
}
28+
29+
/**
30+
* @param AST\SelectExpression $selectExpression
31+
*/
32+
public function doWalkSelectExpression($selectExpression): void
33+
{
34+
$this->doWalkNode($selectExpression->expression);
35+
}
36+
37+
/**
38+
* @param mixed $expr
39+
*/
40+
private function doWalkNode($expr): void
41+
{
42+
if ($expr instanceof AST\AggregateExpression) {
43+
$this->markAggregateFunctionFound();
44+
45+
} elseif ($expr instanceof AST\Functions\FunctionNode) {
46+
if ($this->isAggregateFunction($expr)) {
47+
$this->markAggregateFunctionFound();
48+
}
49+
50+
} elseif ($expr instanceof AST\SimpleArithmeticExpression) {
51+
foreach ($expr->arithmeticTerms as $term) {
52+
$this->doWalkArithmeticTerm($term);
53+
}
54+
55+
} elseif ($expr instanceof AST\ArithmeticTerm) {
56+
$this->doWalkArithmeticTerm($expr);
57+
58+
} elseif ($expr instanceof AST\ArithmeticFactor) {
59+
$this->doWalkArithmeticFactor($expr);
60+
61+
} elseif ($expr instanceof AST\ParenthesisExpression) {
62+
$this->doWalkArithmeticPrimary($expr->expression);
63+
64+
} elseif ($expr instanceof AST\NullIfExpression) {
65+
$this->doWalkNullIfExpression($expr);
66+
67+
} elseif ($expr instanceof AST\CoalesceExpression) {
68+
$this->doWalkCoalesceExpression($expr);
69+
70+
} elseif ($expr instanceof AST\GeneralCaseExpression) {
71+
$this->doWalkGeneralCaseExpression($expr);
72+
73+
} elseif ($expr instanceof AST\SimpleCaseExpression) {
74+
$this->doWalkSimpleCaseExpression($expr);
75+
76+
} elseif ($expr instanceof AST\ArithmeticExpression) {
77+
$this->doWalkArithmeticExpression($expr);
78+
79+
} elseif ($expr instanceof AST\ComparisonExpression) {
80+
$this->doWalkComparisonExpression($expr);
81+
82+
} elseif ($expr instanceof AST\BetweenExpression) {
83+
$this->doWalkBetweenExpression($expr);
84+
}
85+
}
86+
87+
public function doWalkCoalesceExpression(AST\CoalesceExpression $coalesceExpression): void
88+
{
89+
foreach ($coalesceExpression->scalarExpressions as $scalarExpression) {
90+
$this->doWalkSimpleArithmeticExpression($scalarExpression);
91+
}
92+
}
93+
94+
public function doWalkNullIfExpression(AST\NullIfExpression $nullIfExpression): void
95+
{
96+
if (!is_string($nullIfExpression->firstExpression)) {
97+
$this->doWalkSimpleArithmeticExpression($nullIfExpression->firstExpression);
98+
}
99+
100+
if (is_string($nullIfExpression->secondExpression)) {
101+
return;
102+
}
103+
104+
$this->doWalkSimpleArithmeticExpression($nullIfExpression->secondExpression);
105+
}
106+
107+
public function doWalkGeneralCaseExpression(AST\GeneralCaseExpression $generalCaseExpression): void
108+
{
109+
foreach ($generalCaseExpression->whenClauses as $whenClause) {
110+
$this->doWalkConditionalExpression($whenClause->caseConditionExpression);
111+
$this->doWalkSimpleArithmeticExpression($whenClause->thenScalarExpression);
112+
}
113+
114+
$this->doWalkSimpleArithmeticExpression($generalCaseExpression->elseScalarExpression);
115+
}
116+
117+
public function doWalkSimpleCaseExpression(AST\SimpleCaseExpression $simpleCaseExpression): void
118+
{
119+
foreach ($simpleCaseExpression->simpleWhenClauses as $simpleWhenClause) {
120+
$this->doWalkSimpleArithmeticExpression($simpleWhenClause->caseScalarExpression);
121+
$this->doWalkSimpleArithmeticExpression($simpleWhenClause->thenScalarExpression);
122+
}
123+
124+
$this->doWalkSimpleArithmeticExpression($simpleCaseExpression->elseScalarExpression);
125+
}
126+
127+
/**
128+
* @param AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr
129+
*/
130+
public function doWalkConditionalExpression($condExpr): void
131+
{
132+
if (!$condExpr instanceof AST\ConditionalExpression) {
133+
$this->doWalkConditionalTerm($condExpr); // @phpstan-ignore-line PHPStan do not read @psalm-inheritors of Phase2OptimizableConditional
134+
return;
135+
}
136+
137+
foreach ($condExpr->conditionalTerms as $conditionalTerm) {
138+
$this->doWalkConditionalTerm($conditionalTerm);
139+
}
140+
}
141+
142+
/**
143+
* @param AST\ConditionalTerm|AST\ConditionalPrimary|AST\ConditionalFactor $condTerm
144+
*/
145+
public function doWalkConditionalTerm($condTerm): void
146+
{
147+
if (!$condTerm instanceof AST\ConditionalTerm) {
148+
$this->doWalkConditionalFactor($condTerm);
149+
return;
150+
}
151+
152+
foreach ($condTerm->conditionalFactors as $conditionalFactor) {
153+
$this->doWalkConditionalFactor($conditionalFactor);
154+
}
155+
}
156+
157+
/**
158+
* @param AST\ConditionalFactor|AST\ConditionalPrimary $factor
159+
*/
160+
public function doWalkConditionalFactor($factor): void
161+
{
162+
if (!$factor instanceof AST\ConditionalFactor) {
163+
$this->doWalkConditionalPrimary($factor);
164+
} else {
165+
$this->doWalkConditionalPrimary($factor->conditionalPrimary);
166+
}
167+
}
168+
169+
/**
170+
* @param AST\ConditionalPrimary $primary
171+
*/
172+
public function doWalkConditionalPrimary($primary): void
173+
{
174+
if ($primary->isSimpleConditionalExpression()) {
175+
if ($primary->simpleConditionalExpression instanceof AST\ComparisonExpression) {
176+
$this->doWalkComparisonExpression($primary->simpleConditionalExpression);
177+
return;
178+
}
179+
$this->doWalkNode($primary->simpleConditionalExpression);
180+
}
181+
182+
if (!$primary->isConditionalExpression()) {
183+
return;
184+
}
185+
186+
if ($primary->conditionalExpression === null) {
187+
return;
188+
}
189+
190+
$this->doWalkConditionalExpression($primary->conditionalExpression);
191+
}
192+
193+
/**
194+
* @param AST\BetweenExpression $betweenExpr
195+
*/
196+
public function doWalkBetweenExpression($betweenExpr): void
197+
{
198+
$this->doWalkArithmeticExpression($betweenExpr->expression);
199+
$this->doWalkArithmeticExpression($betweenExpr->leftBetweenExpression);
200+
$this->doWalkArithmeticExpression($betweenExpr->rightBetweenExpression);
201+
}
202+
203+
/**
204+
* @param AST\ComparisonExpression $compExpr
205+
*/
206+
public function doWalkComparisonExpression($compExpr): void
207+
{
208+
$leftExpr = $compExpr->leftExpression;
209+
$rightExpr = $compExpr->rightExpression;
210+
211+
if ($leftExpr instanceof AST\Node) {
212+
$this->doWalkNode($leftExpr);
213+
}
214+
215+
if (!($rightExpr instanceof AST\Node)) {
216+
return;
217+
}
218+
219+
$this->doWalkNode($rightExpr);
220+
}
221+
222+
/**
223+
* @param AST\ArithmeticExpression $arithmeticExpr
224+
*/
225+
public function doWalkArithmeticExpression($arithmeticExpr): void
226+
{
227+
if (!$arithmeticExpr->isSimpleArithmeticExpression()) {
228+
return;
229+
}
230+
231+
if ($arithmeticExpr->simpleArithmeticExpression === null) {
232+
return;
233+
}
234+
235+
$this->doWalkSimpleArithmeticExpression($arithmeticExpr->simpleArithmeticExpression);
236+
}
237+
238+
/**
239+
* @param AST\Node|string $simpleArithmeticExpr
240+
*/
241+
public function doWalkSimpleArithmeticExpression($simpleArithmeticExpr): void
242+
{
243+
if (!$simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression) {
244+
$this->doWalkArithmeticTerm($simpleArithmeticExpr);
245+
return;
246+
}
247+
248+
foreach ($simpleArithmeticExpr->arithmeticTerms as $term) {
249+
$this->doWalkArithmeticTerm($term);
250+
}
251+
}
252+
253+
/**
254+
* @param AST\Node|string $term
255+
*/
256+
public function doWalkArithmeticTerm($term): void
257+
{
258+
if (is_string($term)) {
259+
return;
260+
}
261+
262+
if (!$term instanceof AST\ArithmeticTerm) {
263+
$this->doWalkArithmeticFactor($term);
264+
return;
265+
}
266+
267+
foreach ($term->arithmeticFactors as $factor) {
268+
$this->doWalkArithmeticFactor($factor);
269+
}
270+
}
271+
272+
/**
273+
* @param AST\Node|string $factor
274+
*/
275+
public function doWalkArithmeticFactor($factor): void
276+
{
277+
if (is_string($factor)) {
278+
return;
279+
}
280+
281+
if (!$factor instanceof AST\ArithmeticFactor) {
282+
$this->doWalkArithmeticPrimary($factor);
283+
return;
284+
}
285+
286+
$this->doWalkArithmeticPrimary($factor->arithmeticPrimary);
287+
}
288+
289+
/**
290+
* @param AST\Node|string $primary
291+
*/
292+
public function doWalkArithmeticPrimary($primary): void
293+
{
294+
if ($primary instanceof AST\SimpleArithmeticExpression) {
295+
$this->doWalkSimpleArithmeticExpression($primary);
296+
return;
297+
}
298+
299+
if (!($primary instanceof AST\Node)) {
300+
return;
301+
}
302+
303+
$this->doWalkNode($primary);
304+
}
305+
306+
private function isAggregateFunction(AST\Node $node): bool
307+
{
308+
return $node instanceof AST\Functions\AvgFunction
309+
|| $node instanceof AST\Functions\CountFunction
310+
|| $node instanceof AST\Functions\MaxFunction
311+
|| $node instanceof AST\Functions\MinFunction
312+
|| $node instanceof AST\Functions\SumFunction
313+
|| $node instanceof AST\AggregateExpression;
314+
}
315+
316+
private function markAggregateFunctionFound(): void
317+
{
318+
$this->_getQuery()->setHint(self::HINT_HAS_AGGREGATE_FUNCTION, true);
319+
}
320+
321+
}

Diff for: src/Type/Doctrine/Query/QueryResultTypeWalker.php

+3-27
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class QueryResultTypeWalker extends SqlWalker
117117
public static function walk(Query $query, QueryResultTypeBuilder $typeBuilder, DescriptorRegistry $descriptorRegistry): void
118118
{
119119
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, self::class);
120+
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [QueryAggregateFunctionDetectorTreeWalker::class]);
120121
$query->setHint(self::HINT_TYPE_MAPPING, $typeBuilder);
121122
$query->setHint(self::HINT_DESCRIPTOR_REGISTRY, $descriptorRegistry);
122123

@@ -137,7 +138,8 @@ public function __construct($query, $parserResult, array $queryComponents)
137138
$this->em = $query->getEntityManager();
138139
$this->queryComponents = $queryComponents;
139140
$this->nullableQueryComponents = [];
140-
$this->hasAggregateFunction = false;
141+
$this->hasAggregateFunction = $query->hasHint(QueryAggregateFunctionDetectorTreeWalker::HINT_HAS_AGGREGATE_FUNCTION);
142+
141143
$this->hasGroupByClause = false;
142144

143145
// The object is instantiated by Doctrine\ORM\Query\Parser, so receiving
@@ -176,7 +178,6 @@ public function __construct($query, $parserResult, array $queryComponents)
176178
public function walkSelectStatement(AST\SelectStatement $AST): string
177179
{
178180
$this->typeBuilder->setSelectQuery();
179-
$this->hasAggregateFunction = $this->hasAggregateFunction($AST);
180181
$this->hasGroupByClause = $AST->groupByClause !== null;
181182

182183
$this->walkFromClause($AST->fromClause);
@@ -1432,29 +1433,4 @@ private function hasAggregateWithoutGroupBy(): bool
14321433
return $this->hasAggregateFunction && !$this->hasGroupByClause;
14331434
}
14341435

1435-
private function hasAggregateFunction(AST\SelectStatement $AST): bool
1436-
{
1437-
foreach ($AST->selectClause->selectExpressions as $selectExpression) {
1438-
if (!$selectExpression instanceof AST\SelectExpression) {
1439-
continue;
1440-
}
1441-
1442-
$expression = $selectExpression->expression;
1443-
1444-
switch (true) {
1445-
case $expression instanceof AST\Functions\AvgFunction:
1446-
case $expression instanceof AST\Functions\CountFunction:
1447-
case $expression instanceof AST\Functions\MaxFunction:
1448-
case $expression instanceof AST\Functions\MinFunction:
1449-
case $expression instanceof AST\Functions\SumFunction:
1450-
case $expression instanceof AST\AggregateExpression:
1451-
return true;
1452-
default:
1453-
break;
1454-
}
1455-
}
1456-
1457-
return false;
1458-
}
1459-
14601436
}

0 commit comments

Comments
 (0)