Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/1.3.x' into 1.4.x
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Nov 15, 2023
2 parents 39d78ad + b0e0c32 commit 324d3cd
Show file tree
Hide file tree
Showing 14 changed files with 199 additions and 31 deletions.
5 changes: 5 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,8 @@ services:
class: PHPStan\PhpDoc\Doctrine\QueryTypeNodeResolverExtension
tags:
- phpstan.phpDoc.typeNodeResolverExtension

-
class: PHPStan\Type\Doctrine\EntityManagerInterfaceThrowTypeExtension
tags:
- phpstan.dynamicMethodThrowTypeExtension
6 changes: 5 additions & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ parameters:
count: 1
path: tests/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php

-
message: "#^Accessing PHPStan\\\\Rules\\\\Exceptions\\\\TooWideMethodThrowTypeRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#"
count: 1
path: tests/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php

-
message: "#^Accessing PHPStan\\\\Rules\\\\DeadCode\\\\UnusedPrivatePropertyRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#"
count: 1
Expand All @@ -59,4 +64,3 @@ parameters:
message: "#^Accessing PHPStan\\\\Rules\\\\Properties\\\\MissingReadOnlyPropertyAssignRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#"
count: 1
path: tests/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php

1 change: 1 addition & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ services:
class: PHPStan\Rules\Doctrine\ORM\QueryBuilderDqlRule
arguments:
reportDynamicQueryBuilders: %doctrine.reportDynamicQueryBuilders%
searchOtherMethodsForQueryBuilderBeginning: %doctrine.searchOtherMethodsForQueryBuilderBeginning%
tags:
- phpstan.rules.rule
-
Expand Down
21 changes: 20 additions & 1 deletion src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Doctrine\DoctrineTypeUtils;
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
use PHPStan\Type\Doctrine\QueryBuilder\OtherMethodQueryBuilderParser;
use PHPStan\Type\ObjectType;
use PHPStan\Type\TypeUtils;
use Throwable;
Expand All @@ -31,13 +32,23 @@ class QueryBuilderDqlRule implements Rule
/** @var bool */
private $reportDynamicQueryBuilders;

/** @var OtherMethodQueryBuilderParser */
private $otherMethodQueryBuilderParser;

/** @var bool */
private $searchOtherMethodsForQueryBuilderBeginning;

public function __construct(
ObjectMetadataResolver $objectMetadataResolver,
bool $reportDynamicQueryBuilders
OtherMethodQueryBuilderParser $otherMethodQueryBuilderParser,
bool $reportDynamicQueryBuilders,
bool $searchOtherMethodsForQueryBuilderBeginning
)
{
$this->objectMetadataResolver = $objectMetadataResolver;
$this->otherMethodQueryBuilderParser = $otherMethodQueryBuilderParser;
$this->reportDynamicQueryBuilders = $reportDynamicQueryBuilders;
$this->searchOtherMethodsForQueryBuilderBeginning = $searchOtherMethodsForQueryBuilderBeginning;
}

public function getNodeType(): string
Expand All @@ -58,6 +69,14 @@ public function processNode(Node $node, Scope $scope): array
$calledOnType = $scope->getType($node->var);
$queryBuilderTypes = DoctrineTypeUtils::getQueryBuilderTypes($calledOnType);
if (count($queryBuilderTypes) === 0) {

if ($this->searchOtherMethodsForQueryBuilderBeginning) {
$queryBuilderTypes = $this->otherMethodQueryBuilderParser->getQueryBuilderTypes($scope, $node);
if (count($queryBuilderTypes) !== 0) {
return [];
}
}

if (
$this->reportDynamicQueryBuilders
&& (new ObjectType('Doctrine\ORM\QueryBuilder'))->isSuperTypeOf($calledOnType)->yes()
Expand Down
49 changes: 49 additions & 0 deletions src/Type/Doctrine/EntityManagerInterfaceThrowTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Doctrine;

use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\Persistence\ObjectManager;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodThrowTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_map;

class EntityManagerInterfaceThrowTypeExtension implements DynamicMethodThrowTypeExtension
{

public const SUPPORTED_METHOD = [
'flush' => [
ORMException::class,
UniqueConstraintViolationException::class,
],
];

public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getDeclaringClass()->getName() === ObjectManager::class
&& isset(self::SUPPORTED_METHOD[$methodReflection->getName()]);
}

public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
$type = $scope->getType($methodCall->var);

if ((new ObjectType(EntityManagerInterface::class))->isSuperTypeOf($type)->yes()) {
return TypeCombinator::union(
...array_map(static function ($class): Type {
return new ObjectType($class);
}, self::SUPPORTED_METHOD[$methodReflection->getName()])
);
}

return $methodReflection->getThrowType();
}

}
19 changes: 15 additions & 4 deletions src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
use PHPStan\Parser\Parser;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;
use function count;
use function is_array;

Expand Down Expand Up @@ -118,11 +122,18 @@ private function findQueryBuilderTypesInCalledMethod(Scope $scope, MethodCall $m
}

$exprType = $scope->getType($node->expr);
if (!$exprType instanceof QueryBuilderType) {
return;
}

$queryBuilderTypes[] = $exprType;
TypeTraverser::map($exprType, static function (Type $type, callable $traverse) use (&$queryBuilderTypes): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}

if ($type instanceof QueryBuilderType) {
$queryBuilderTypes[] = $type;
}

return $type;
});
});

return $queryBuilderTypes;
Expand Down
15 changes: 8 additions & 7 deletions stubs/EntityManagerInterface.stub
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,12 @@ interface EntityManagerInterface extends ObjectManager
*/
public function copy($entity, $deep = false);

/**
* @template T of object
* @phpstan-param class-string<T> $className
*
* @phpstan-return ClassMetadata<T>
*/
public function getClassMetadata($className);
/**
* @template T of object
* @phpstan-param class-string<T> $className
*
* @phpstan-return ClassMetadata<T>
*/
public function getClassMetadata($className);

}
12 changes: 6 additions & 6 deletions stubs/bleedingEdge/ORM/QueryBuilder.stub
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ class QueryBuilder
}

/**
* @param literal-string $from
* @param literal-string $alias
* @param literal-string|null $indexBy
* @param literal-string|class-string $from
* @param literal-string $alias
* @param literal-string|null $indexBy
*
* @return $this
*/
Expand All @@ -70,7 +70,7 @@ class QueryBuilder
}

/**
* @param literal-string $join
* @param literal-string|class-string $join
* @param literal-string $alias
* @param Expr\Join::ON|Expr\Join::WITH|null $conditionType
* @param literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition
Expand All @@ -84,7 +84,7 @@ class QueryBuilder
}

/**
* @param literal-string $join
* @param literal-string|class-string $join
* @param literal-string $alias
* @param Expr\Join::ON|Expr\Join::WITH|null $conditionType
* @param literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition
Expand All @@ -98,7 +98,7 @@ class QueryBuilder
}

/**
* @param literal-string $join
* @param literal-string|class-string $join
* @param literal-string $alias
* @param Expr\Join::ON|Expr\Join::WITH|null $conditionType
* @param literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition
Expand Down
12 changes: 7 additions & 5 deletions tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
use PHPStan\Type\Doctrine\QueryBuilder\OtherMethodQueryBuilderParser;

/**
* @extends RuleTestCase<QueryBuilderDqlRule>
Expand All @@ -14,7 +15,12 @@ class QueryBuilderDqlRuleSlowTest extends RuleTestCase

protected function getRule(): Rule
{
return new QueryBuilderDqlRule(new ObjectMetadataResolver(__DIR__ . '/entity-manager.php'), true);
return new QueryBuilderDqlRule(
new ObjectMetadataResolver(__DIR__ . '/entity-manager.php'),
self::getContainer()->getByType(OtherMethodQueryBuilderParser::class),
true,
true
);
}

public function testRule(): void
Expand All @@ -40,10 +46,6 @@ public function testRule(): void
'QueryBuilder: [Semantical Error] line 0, col 14 near \'Foo e\': Error: Class \'Foo\' is not defined.',
71,
],
[
'Could not analyse QueryBuilder with unknown beginning.',
89,
],
[
'Could not analyse QueryBuilder with dynamic arguments.',
99,
Expand Down
12 changes: 7 additions & 5 deletions tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
use PHPStan\Type\Doctrine\QueryBuilder\OtherMethodQueryBuilderParser;

/**
* @extends RuleTestCase<QueryBuilderDqlRule>
Expand All @@ -14,7 +15,12 @@ class QueryBuilderDqlRuleTest extends RuleTestCase

protected function getRule(): Rule
{
return new QueryBuilderDqlRule(new ObjectMetadataResolver(__DIR__ . '/entity-manager.php'), true);
return new QueryBuilderDqlRule(
new ObjectMetadataResolver(__DIR__ . '/entity-manager.php'),
self::getContainer()->getByType(OtherMethodQueryBuilderParser::class),
true,
true
);
}

public function testRule(): void
Expand All @@ -40,10 +46,6 @@ public function testRule(): void
'QueryBuilder: [Semantical Error] line 0, col 14 near \'Foo e\': Error: Class \'Foo\' is not defined.',
71,
],
[
'Could not analyse QueryBuilder with unknown beginning.',
89,
],
[
'Could not analyse QueryBuilder with dynamic arguments.',
99,
Expand Down
31 changes: 31 additions & 0 deletions tests/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Exceptions;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<TooWideMethodThrowTypeRule>
*/
class TooWideMethodThrowTypeRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return self::getContainer()->getByType(TooWideMethodThrowTypeRule::class);
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/entity-manager-interface.php'], []);
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/../../../extension.neon',
];
}

}
21 changes: 21 additions & 0 deletions tests/Rules/Exceptions/data/entity-manager-interface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace EntityManagerInterfaceThrowTypeExtensionTest;

use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Exception\ORMException;

class Example
{

/**
* @throws ORMException
* @throws UniqueConstraintViolationException
*/
public function doFoo(EntityManagerInterface $entityManager): void
{
$entityManager->flush();
}

}
8 changes: 8 additions & 0 deletions tests/Rules/Exceptions/data/unthrown-exception.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@ class FooFacade
/** @var \Doctrine\ORM\EntityManager */
private $entityManager;

/** @var \Doctrine\ORM\EntityManagerInterface */
private $entityManagerInterface;

public function doFoo(): void
{
try {
$this->entityManager->flush();
} catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) {
// pass
}
try {
$this->entityManagerInterface->flush();
} catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) {
// pass
}
}

}
18 changes: 16 additions & 2 deletions tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,11 @@ public function testQueryResultTypeIsVoidWithDeleteOrUpdate(EntityManagerInterfa

public function testQueryTypeIsInferredOnAcrossMethods(EntityManagerInterface $em): void
{
$query = $this->getQueryBuilder($em)
->getQuery();
$query = $this->getQueryBuilder($em)->getQuery();
$branchingQuery = $this->getBranchingQueryBuilder($em)->getQuery();

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

private function getQueryBuilder(EntityManagerInterface $em): QueryBuilder
Expand All @@ -152,4 +153,17 @@ private function getQueryBuilder(EntityManagerInterface $em): QueryBuilder
->select('m')
->from(Many::class, 'm');
}

private function getBranchingQueryBuilder(EntityManagerInterface $em): QueryBuilder
{
$queryBuilder = $em->createQueryBuilder()
->select('m')
->from(Many::class, 'm');

if (random_int(0, 1) === 1) {
$queryBuilder->andWhere('m.intColumn = 1');
}

return $queryBuilder;
}
}

0 comments on commit 324d3cd

Please sign in to comment.