diff --git a/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php b/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php index 5e0030cd..0cc99224 100644 --- a/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php +++ b/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php @@ -15,6 +15,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\TypeUtils; use Throwable; +use function array_values; use function count; use function sprintf; use function strpos; @@ -117,7 +118,8 @@ public function processNode(Node $node, Scope $scope): array $message .= sprintf("\nDQL: %s", $dql->getValue()); } - $messages[] = RuleErrorBuilder::message($message) + // Use message as index to prevent duplicate + $messages[$message] = RuleErrorBuilder::message($message) ->identifier('doctrine.dql') ->build(); } catch (AssertionError $e) { @@ -125,7 +127,7 @@ public function processNode(Node $node, Scope $scope): array } } - return $messages; + return array_values($messages); } } diff --git a/src/Type/Doctrine/Query/QueryType.php b/src/Type/Doctrine/Query/QueryType.php index 5fc24df5..b0cb7968 100644 --- a/src/Type/Doctrine/Query/QueryType.php +++ b/src/Type/Doctrine/Query/QueryType.php @@ -11,15 +11,25 @@ class QueryType extends GenericObjectType { + /** @var Type */ + private $indexType; + + /** @var Type */ + private $resultType; + /** @var string */ private $dql; - public function __construct(string $dql, ?Type $indexType = null, ?Type $resultType = null) + public function __construct(string $dql, ?Type $indexType = null, ?Type $resultType = null, ?Type $subtractedType = null) { + $this->indexType = $indexType ?? new MixedType(); + $this->resultType = $resultType ?? new MixedType(); + parent::__construct('Doctrine\ORM\Query', [ - $indexType ?? new MixedType(), - $resultType ?? new MixedType(), - ]); + $this->indexType, + $this->resultType, + ], $subtractedType); + $this->dql = $dql; } @@ -32,6 +42,11 @@ public function equals(Type $type): bool return parent::equals($type); } + public function changeSubtractedType(?Type $subtractedType): Type + { + return new self('Doctrine\ORM\Query', $this->indexType, $this->resultType, $subtractedType); + } + public function isSuperTypeOf(Type $type): TrinaryLogic { if ($type instanceof self) { diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php index 7c4f4d40..e633b464 100644 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php +++ b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php @@ -109,14 +109,18 @@ public function testRuleBranches(): void 'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1\': Error: \'p\' is not defined.', 59, ], - /*[ + [ 'QueryBuilder: [Semantical Error] line 0, col 93 near \'t.id = 1\': Error: \'t\' is not defined.', 90, - ],*/ + ], [ 'QueryBuilder: [Semantical Error] line 0, col 95 near \'foo = 1\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named foo', 107, ], + [ + 'QueryBuilder: [Semantical Error] line 0, col 93 near \'t.id = 1\': Error: \'t\' is not defined.', + 107, + ], ]; $this->analyse([__DIR__ . '/data/query-builder-branches-dql.php'], $errors); } diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php index 37124635..28700630 100644 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php +++ b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php @@ -109,14 +109,18 @@ public function testRuleBranches(): void 'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1\': Error: \'p\' is not defined.', 59, ], - /*[ + [ 'QueryBuilder: [Semantical Error] line 0, col 93 near \'t.id = 1\': Error: \'t\' is not defined.', 90, - ],*/ + ], [ 'QueryBuilder: [Semantical Error] line 0, col 95 near \'foo = 1\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named foo', 107, ], + [ + 'QueryBuilder: [Semantical Error] line 0, col 93 near \'t.id = 1\': Error: \'t\' is not defined.', + 107, + ], ]; $this->analyse([__DIR__ . '/data/query-builder-branches-dql.php'], $errors); } diff --git a/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php b/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php index f6197db8..2a6af1d9 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php +++ b/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php @@ -26,7 +26,7 @@ public function testQueryTypeIsInferredOnAcrossMethods(EntityManagerInterface $e $branchingQuery = $this->getBranchingQueryBuilder($em)->getQuery(); assertType('Doctrine\ORM\Query', $query); - assertType('Doctrine\ORM\Query', $branchingQuery); + assertType('Doctrine\ORM\Query#1|Doctrine\ORM\Query#2', $branchingQuery); } public function testQueryTypeIsInferredOnAcrossMethodsEvenWhenVariableAssignmentIsUsed(EntityManagerInterface $em): void diff --git a/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php b/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php index d88ba764..c0cf3068 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php +++ b/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php @@ -103,6 +103,19 @@ public function testIndexByResultInfering(EntityManagerInterface $em): void assertType('array', $result); } + public function testConditionalAddSelect(EntityManagerInterface $em, bool $bool): void + { + $qb = $em->createQueryBuilder(); + if ($bool) { + $qb->select('m.intColumn'); + } else { + $qb->select('m.intColumn', 'm.stringNullColumn'); + } + $query = $qb->from(Many::class, 'm')->getQuery(); + + assertType('Doctrine\ORM\Query|Doctrine\ORM\Query', $query); + } + public function testQueryResultTypeIsMixedWhenDQLIsNotKnown(QueryBuilder $builder): void { $query = $builder->getQuery();