Skip to content

Commit e7915fd

Browse files
committed
Option to use the more expensive computation (off by default)
1 parent ad25bae commit e7915fd

File tree

8 files changed

+265
-42
lines changed

8 files changed

+265
-42
lines changed

extension.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@ parameters:
55
allCollectionsSelectable: true
66
objectManagerLoader: null
77

8+
# setting this to false might lead to performance problems
9+
# it changes the braching logic - with false, queryBuilders from all branches are analysed separately
10+
queryBuilderFastAlgorithm: true
11+
812
parametersSchema:
913
doctrine: structure([
1014
repositoryClass: schema(string(), nullable())
1115
queryBuilderClass: schema(string(), nullable())
1216
allCollectionsSelectable: bool()
1317
objectManagerLoader: schema(string(), nullable())
18+
queryBuilderFastAlgorithm: bool()
1419
])
1520

1621
conditionalTags:
@@ -28,6 +33,7 @@ services:
2833
class: PHPStan\Type\Doctrine\QueryBuilder\CreateQueryBuilderDynamicReturnTypeExtension
2934
arguments:
3035
queryBuilderClass: %doctrine.queryBuilderClass%
36+
fasterVersion: %doctrine.queryBuilderFastAlgorithm%
3137
tags:
3238
- phpstan.broker.dynamicMethodReturnTypeExtension
3339
-

rules.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ parametersSchema:
88
queryBuilderClass: schema(string(), nullable())
99
allCollectionsSelectable: bool()
1010
objectManagerLoader: schema(string(), nullable())
11+
queryBuilderFastAlgorithm: bool()
1112
reportDynamicQueryBuilders: bool()
1213
])
1314

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\QueryBuilder;
4+
5+
use PHPStan\TrinaryLogic;
6+
use PHPStan\Type\Type;
7+
8+
class BranchingQueryBuilderType extends QueryBuilderType
9+
{
10+
11+
public function equals(Type $type): bool
12+
{
13+
if ($type instanceof parent) {
14+
if (count($this->getMethodCalls()) !== count($type->getMethodCalls())) {
15+
return false;
16+
}
17+
18+
foreach ($this->getMethodCalls() as $id => $methodCall) {
19+
if (!isset($type->getMethodCalls()[$id])) {
20+
return false;
21+
}
22+
}
23+
24+
foreach ($type->getMethodCalls() as $id => $methodCall) {
25+
if (!isset($this->getMethodCalls()[$id])) {
26+
return false;
27+
}
28+
}
29+
30+
return true;
31+
}
32+
33+
return parent::equals($type);
34+
}
35+
36+
public function isSuperTypeOf(Type $type): TrinaryLogic
37+
{
38+
if ($type instanceof parent) {
39+
return TrinaryLogic::createFromBoolean($this->equals($type));
40+
}
41+
42+
return parent::isSuperTypeOf($type);
43+
}
44+
45+
}

src/Type/Doctrine/QueryBuilder/CreateQueryBuilderDynamicReturnTypeExtension.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,16 @@ class CreateQueryBuilderDynamicReturnTypeExtension implements \PHPStan\Type\Dyna
1313
/** @var string|null */
1414
private $queryBuilderClass;
1515

16-
public function __construct(?string $queryBuilderClass)
16+
/** @var bool */
17+
private $fasterVersion;
18+
19+
public function __construct(
20+
?string $queryBuilderClass,
21+
bool $fasterVersion
22+
)
1723
{
1824
$this->queryBuilderClass = $queryBuilderClass;
25+
$this->fasterVersion = $fasterVersion;
1926
}
2027

2128
public function getClass(): string
@@ -34,7 +41,12 @@ public function getTypeFromMethodCall(
3441
Scope $scope
3542
): Type
3643
{
37-
return new QueryBuilderType(
44+
$class = SimpleQueryBuilderType::class;
45+
if (!$this->fasterVersion) {
46+
$class = BranchingQueryBuilderType::class;
47+
}
48+
49+
return new $class(
3850
$this->queryBuilderClass ?? 'Doctrine\ORM\QueryBuilder'
3951
);
4052
}

src/Type/Doctrine/QueryBuilder/QueryBuilderType.php

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
namespace PHPStan\Type\Doctrine\QueryBuilder;
44

55
use PhpParser\Node\Expr\MethodCall;
6-
use PHPStan\TrinaryLogic;
76
use PHPStan\Type\ObjectType;
8-
use PHPStan\Type\Type;
97

10-
class QueryBuilderType extends ObjectType
8+
abstract class QueryBuilderType extends ObjectType
119
{
1210

1311
/** @var array<string, MethodCall> */
@@ -21,37 +19,9 @@ public function getMethodCalls(): array
2119
return $this->methodCalls;
2220
}
2321

24-
public function equals(Type $type): bool
25-
{
26-
if ($type instanceof self) {
27-
if (count($this->methodCalls) !== count($type->methodCalls)) {
28-
return false;
29-
}
30-
31-
foreach ($this->getMethodCalls() as $id => $methodCall) {
32-
if (!isset($type->methodCalls[$id])) {
33-
return false;
34-
}
35-
}
36-
37-
return true;
38-
}
39-
40-
return parent::equals($type);
41-
}
42-
43-
public function isSuperTypeOf(Type $type): TrinaryLogic
44-
{
45-
if ($type instanceof self) {
46-
return TrinaryLogic::createFromBoolean($this->equals($type));
47-
}
48-
49-
return parent::isSuperTypeOf($type);
50-
}
51-
5222
public function append(MethodCall $methodCall): self
5323
{
54-
$object = new self($this->getClassName());
24+
$object = new static($this->getClassName());
5525
$object->methodCalls = $this->methodCalls;
5626
$object->methodCalls[substr(md5(uniqid()), 0, 10)] = $methodCall;
5727

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine\QueryBuilder;
4+
5+
use PHPStan\TrinaryLogic;
6+
use PHPStan\Type\Type;
7+
8+
class SimpleQueryBuilderType extends QueryBuilderType
9+
{
10+
11+
public function equals(Type $type): bool
12+
{
13+
if ($type instanceof parent) {
14+
return count($this->getMethodCalls()) === count($type->getMethodCalls());
15+
}
16+
17+
return parent::equals($type);
18+
}
19+
20+
public function isSuperTypeOf(Type $type): TrinaryLogic
21+
{
22+
if ($type instanceof parent) {
23+
$thisCount = count($this->getMethodCalls());
24+
$thatCount = count($type->getMethodCalls());
25+
26+
return TrinaryLogic::createFromBoolean($thisCount >= $thatCount);
27+
}
28+
29+
return parent::isSuperTypeOf($type);
30+
}
31+
32+
}

tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@
1919
class QueryBuilderDqlRuleTest extends RuleTestCase
2020
{
2121

22+
/** @var bool */
23+
private $fasterVersion;
24+
2225
protected function getRule(): Rule
2326
{
2427
return new QueryBuilderDqlRule(new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', null), true);
2528
}
2629

2730
public function testRule(): void
2831
{
32+
$this->fasterVersion = false;
2933
$this->analyse([__DIR__ . '/data/query-builder-dql.php'], [
3034
[
3135
"QueryBuilder: [Syntax Error] line 0, col 66: Error: Expected end of string, got ')'\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE e.id = 1)",
@@ -94,9 +98,22 @@ public function testRule(): void
9498
]);
9599
}
96100

97-
public function testRuleBranches(): void
101+
/**
102+
* @return bool[][]
103+
*/
104+
public function dataRuleBranches(): array
98105
{
99-
$this->analyse([__DIR__ . '/data/query-builder-branches-dql.php'], [
106+
return [[true], [false]];
107+
}
108+
109+
/**
110+
* @dataProvider dataRuleBranches
111+
* @param bool $fasterVersion
112+
*/
113+
public function testRuleBranches(bool $fasterVersion): void
114+
{
115+
$this->fasterVersion = $fasterVersion;
116+
$errors = [
100117
[
101118
'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1\': Error: \'p\' is not defined.',
102119
31,
@@ -105,10 +122,6 @@ public function testRuleBranches(): void
105122
'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1\': Error: \'p\' is not defined.',
106123
45,
107124
],
108-
[
109-
'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1\': Error: \'p\' is not defined.',
110-
59,
111-
],
112125
[
113126
'QueryBuilder: [Semantical Error] line 0, col 93 near \'t.id = 1\': Error: \'t\' is not defined.',
114127
90,
@@ -117,9 +130,29 @@ public function testRuleBranches(): void
117130
'QueryBuilder: [Semantical Error] line 0, col 95 near \'foo = 1\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named foo',
118131
107,
119132
],
120-
[
133+
];
134+
if (!$fasterVersion) {
135+
array_splice($errors, 2, 0, [
136+
[
137+
'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1\': Error: \'p\' is not defined.',
138+
59,
139+
],
140+
]);
141+
$errors[] = [
121142
'QueryBuilder: [Semantical Error] line 0, col 93 near \'t.id = 1\': Error: \'t\' is not defined.',
122143
107,
144+
];
145+
}
146+
$this->analyse([__DIR__ . '/data/query-builder-branches-dql.php'], $errors);
147+
}
148+
149+
public function testBranchingPerformance(): void
150+
{
151+
$this->fasterVersion = true;
152+
$this->analyse([__DIR__ . '/data/query-builder-branches-performance.php'], [
153+
[
154+
'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1 AND\': Error: \'p\' is not defined.',
155+
121,
123156
],
124157
]);
125158
}
@@ -132,7 +165,7 @@ public function getDynamicMethodReturnTypeExtensions(): array
132165
$objectMetadataResolver = new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', null);
133166
$argumentsProcessor = new ArgumentsProcessor();
134167
return [
135-
new CreateQueryBuilderDynamicReturnTypeExtension(null),
168+
new CreateQueryBuilderDynamicReturnTypeExtension(null, $this->fasterVersion),
136169
new QueryBuilderMethodDynamicReturnTypeExtension(null),
137170
new QueryBuilderGetQueryDynamicReturnTypeExtension($objectMetadataResolver, $argumentsProcessor, null),
138171
new QueryGetDqlDynamicReturnTypeExtension(),

0 commit comments

Comments
 (0)