diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ad7800f4..8055b2f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,7 @@ jobs: runs-on: "ubuntu-latest" strategy: + fail-fast: false matrix: php-version: - "7.1" @@ -36,12 +37,16 @@ jobs: - name: "Validate Composer" run: "composer validate" - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress" - - name: "Downgrade PHPUnit" if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" + run: "composer require --dev phpunit/phpunit:^7.5.20 --no-update --update-with-dependencies" + + - name: "Downgrade Doctrine ORM" + if: matrix.php-version == '7.1' + run: "composer require --dev doctrine/orm:^2.7.5 doctrine/lexer:^1.0 --no-update --update-with-dependencies" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" - name: "Lint" run: "make lint" @@ -86,6 +91,7 @@ jobs: - "7.3" - "7.4" - "8.0" + - "8.1" dependencies: - "lowest" - "highest" @@ -100,6 +106,14 @@ jobs: coverage: "none" php-version: "${{ matrix.php-version }}" + - name: "Downgrade PHPUnit" + if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' + run: "composer require --dev phpunit/phpunit:^7.5.20 --no-update --update-with-dependencies" + + - name: "Downgrade Doctrine ORM" + if: matrix.php-version == '7.1' + run: "composer require --dev doctrine/orm:^2.7.5 doctrine/lexer:^1.0 --no-update --update-with-dependencies" + - name: "Install lowest dependencies" if: ${{ matrix.dependencies == 'lowest' }} run: "composer update --prefer-lowest --no-interaction --no-progress" @@ -108,10 +122,6 @@ jobs: if: ${{ matrix.dependencies == 'highest' }} run: "composer update --no-interaction --no-progress" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - - name: "Tests" run: "make tests" @@ -128,6 +138,7 @@ jobs: - "7.3" - "7.4" - "8.0" + - "8.1" dependencies: - "lowest" - "highest" @@ -144,6 +155,14 @@ jobs: extensions: mbstring tools: composer:v2 + - name: "Downgrade PHPUnit" + if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' + run: "composer require --dev phpunit/phpunit:^7.5.20 --no-update --update-with-dependencies" + + - name: "Downgrade Doctrine ORM" + if: matrix.php-version == '7.1' + run: "composer require --dev doctrine/orm:^2.7.5 doctrine/lexer:^1.0 --no-update --update-with-dependencies" + - name: "Install lowest dependencies" if: ${{ matrix.dependencies == 'lowest' }} run: "composer update --prefer-lowest --no-interaction --no-progress" @@ -152,9 +171,5 @@ jobs: if: ${{ matrix.dependencies == 'highest' }} run: "composer update --no-interaction --no-progress" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.1' || matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^7.5.20 --update-with-dependencies" - - name: "PHPStan" run: "make phpstan" diff --git a/build-cs/composer.json b/build-cs/composer.json index ed7744e1..cc6a4983 100644 --- a/build-cs/composer.json +++ b/build-cs/composer.json @@ -3,5 +3,10 @@ "consistence-community/coding-standard": "^3.10", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "slevomat/coding-standard": "^6.4" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/composer.json b/composer.json index 9def1c35..92ad45cf 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "doctrine/collections": "^1.6", "doctrine/common": "^2.7 || ^3.0", "doctrine/dbal": "^2.13.1", + "doctrine/lexer": "^1.2.1", "doctrine/mongodb-odm": "^1.3 || ^2.1", "doctrine/orm": "^2.9.1", "doctrine/persistence": "^1.1 || ^2.0", @@ -29,14 +30,14 @@ "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", + "phpunit/phpunit": "^9.5.10", "ramsey/uuid-doctrine": "^1.5.0", "symfony/cache": "^4.4.35" }, "config": { "platform": { - "php": "7.3.24", - "ext-mongo": "1.6.16" + "ext-mongo": "1.12", + "ext-mongodb": "1.6.16" }, "sort-packages": true, "allow-plugins": { diff --git a/phpstan.neon b/phpstan.neon index d187fc93..a8cde690 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,6 +9,7 @@ includes: parameters: excludePaths: - tests/*/data/* + - tests/*/data-php-*/* ignoreErrors: - diff --git a/tests/DoctrineIntegration/ORM/EntityManagerIntegrationTest.php b/tests/DoctrineIntegration/ORM/EntityManagerIntegrationTest.php index 8134d738..15df0da0 100644 --- a/tests/DoctrineIntegration/ORM/EntityManagerIntegrationTest.php +++ b/tests/DoctrineIntegration/ORM/EntityManagerIntegrationTest.php @@ -14,7 +14,6 @@ public function dataTopics(): array { return [ ['entityManagerDynamicReturn'], - ['entityRepositoryDynamicReturn'], ['entityManagerMergeReturn'], ['customRepositoryUsage'], ['queryBuilder'], diff --git a/tests/DoctrineIntegration/ORM/EntityRepositoryDynamicReturnIntegrationTest.php b/tests/DoctrineIntegration/ORM/EntityRepositoryDynamicReturnIntegrationTest.php new file mode 100644 index 00000000..baf7f6d0 --- /dev/null +++ b/tests/DoctrineIntegration/ORM/EntityRepositoryDynamicReturnIntegrationTest.php @@ -0,0 +1,39 @@ +::findOneBy() - entity PHPStan\\DoctrineIntegration\\ORM\\EntityRepositoryDynamicReturn\\MyEntity does not have a field named $blah.", + "line": 94, + "ignorable": true + }, + { + "message": "Call to method Doctrine\\ORM\\EntityRepository::findBy() - entity PHPStan\\DoctrineIntegration\\ORM\\EntityRepositoryDynamicReturn\\MyEntity does not have a field named $blah.", + "line": 116, + "ignorable": true + }, + { + "message": "Method Doctrine\\ORM\\EntityRepository::createQueryBuilder() invoked with 0 parameters, 1-2 required.", + "line": 239, + "ignorable": true + }, + { + "message": "Could not analyse QueryBuilder with unknown beginning.", + "line": 241, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-2.json b/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-2.json new file mode 100644 index 00000000..33a29c5b --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-2.json @@ -0,0 +1,47 @@ +[ + { + "message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\EntityRepositoryDynamicReturn\\MyEntity::doSomethingElse().", + "line": 89, + "ignorable": true + }, + { + "message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\EntityRepositoryDynamicReturn\\MyEntity::doSomethingElse().", + "line": 101, + "ignorable": true + }, + { + "message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\EntityRepositoryDynamicReturn\\MyEntity::doSomethingElse().", + "line": 110, + "ignorable": true + }, + { + "message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\EntityRepositoryDynamicReturn\\MyEntity::doSomethingElse().", + "line": 120, + "ignorable": true + }, + { + "message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\EntityRepositoryDynamicReturn\\MyEntity::doSomethingElse().", + "line": 142, + "ignorable": true + }, + { + "message": "Call to an undefined method Doctrine\\ORM\\EntityRepository::findByNonexistent().", + "line": 148, + "ignorable": true + }, + { + "message": "Call to an undefined method PHPStan\\DoctrineIntegration\\ORM\\EntityRepositoryDynamicReturn\\MyEntity::doSomethingElse().", + "line": 160, + "ignorable": true + }, + { + "message": "Call to an undefined method Doctrine\\ORM\\EntityRepository::findOneByNonexistent().", + "line": 165, + "ignorable": true + }, + { + "message": "Call to an undefined method Doctrine\\ORM\\EntityRepository::countByNonexistent().", + "line": 174, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-4.json b/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-4.json new file mode 100644 index 00000000..014b3cd4 --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-4.json @@ -0,0 +1,7 @@ +[ + { + "message": "Strict comparison using === between int and 'foo' will always evaluate to false.", + "line": 171, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-5.json b/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-5.json new file mode 100644 index 00000000..3e33332c --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-5.json @@ -0,0 +1,7 @@ +[ + { + "message": "Parameter #1 $className of method Doctrine\\Persistence\\ObjectManager::getRepository() expects class-string, string given.", + "line": 212, + "ignorable": true + } +] diff --git a/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-6.json b/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-6.json new file mode 100644 index 00000000..2452f6e6 --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-6.json @@ -0,0 +1,12 @@ +[ + { + "message": "Property PHPStan\\DoctrineIntegration\\ORM\\EntityRepositoryDynamicReturn\\Example::$repository with generic class Doctrine\\ORM\\EntityRepository does not specify its types: TEntityClass", + "line": 16, + "ignorable": true + }, + { + "message": "Class PHPStan\\DoctrineIntegration\\ORM\\EntityRepositoryDynamicReturn\\Bug180Repository extends generic class Doctrine\\ORM\\EntityRepository but does not specify its types: TEntityClass", + "line": 232, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-7.json b/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-7.json new file mode 100644 index 00000000..1ff56542 --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-7.json @@ -0,0 +1,42 @@ +[ + { + "message": "Call to an undefined method object::doSomething().", + "line": 31, + "ignorable": true + }, + { + "message": "Call to an undefined method object::doSomethingElse().", + "line": 32, + "ignorable": true + }, + { + "message": "Call to an undefined method object::doSomething().", + "line": 43, + "ignorable": true + }, + { + "message": "Call to an undefined method object::doSomethingElse().", + "line": 44, + "ignorable": true + }, + { + "message": "Call to an undefined method object::doSomething().", + "line": 52, + "ignorable": true + }, + { + "message": "Call to an undefined method object::doSomethingElse().", + "line": 53, + "ignorable": true + }, + { + "message": "Call to an undefined method object::doSomething().", + "line": 62, + "ignorable": true + }, + { + "message": "Call to an undefined method object::doSomethingElse().", + "line": 63, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-9.json b/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-9.json new file mode 100644 index 00000000..28906713 --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn-9.json @@ -0,0 +1,7 @@ +[ + { + "message": "Cannot cast mixed to int.", + "line": 241, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn.php b/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn.php new file mode 100644 index 00000000..85248cf1 --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data-php-7.1/entityRepositoryDynamicReturn.php @@ -0,0 +1,243 @@ +repository = $entityManager->getRepository(MyEntity::class); + } + + public function findDynamicType(): void + { + $test = $this->repository->find(1); + + if ($test === null) { + throw new RuntimeException('Sorry, but no...'); + } + + $test->doSomething(); + $test->doSomethingElse(); + } + + public function findOneByDynamicType(): void + { + $test = $this->repository->findOneBy(['blah' => 'testing']); + + if ($test === null) { + throw new RuntimeException('Sorry, but no...'); + } + + $test->doSomething(); + $test->doSomethingElse(); + } + + public function findAllDynamicType(): void + { + $items = $this->repository->findAll(); + + foreach ($items as $test) { + $test->doSomething(); + $test->doSomethingElse(); + } + } + + public function findByDynamicType(): void + { + $items = $this->repository->findBy(['blah' => 'testing']); + + foreach ($items as $test) { + $test->doSomething(); + $test->doSomethingElse(); + } + } +} + +class Example2 +{ + /** + * @var EntityRepository + */ + private $repository; + + public function __construct(EntityManagerInterface $entityManager) + { + $this->repository = $entityManager->getRepository(MyEntity::class); + } + + public function findDynamicType(): void + { + $test = $this->repository->find(1); + + if ($test === null) { + throw new RuntimeException('Sorry, but no...'); + } + + $test->doSomething(); + $test->doSomethingElse(); + } + + public function findOneByDynamicType(): void + { + $test = $this->repository->findOneBy(['blah' => 'testing']); + + if ($test === null) { + throw new RuntimeException('Sorry, but no...'); + } + + $test->doSomething(); + $test->doSomethingElse(); + } + + public function findAllDynamicType(): void + { + $items = $this->repository->findAll(); + + foreach ($items as $test) { + $test->doSomething(); + $test->doSomethingElse(); + } + } + + public function findByDynamicType(): void + { + $items = $this->repository->findBy(['blah' => 'testing']); + + foreach ($items as $test) { + $test->doSomething(); + $test->doSomethingElse(); + } + } +} + +class Example3MagicMethods +{ + /** + * @var EntityRepository + */ + private $repository; + + public function __construct(EntityManagerInterface $entityManager) + { + $this->repository = $entityManager->getRepository(MyEntity::class); + } + + public function findDynamicType(): void + { + $test = $this->repository->findById(1); + foreach ($test as $item) { + $item->doSomething(); + $item->doSomethingElse(); + } + } + + public function findDynamicType2(): void + { + $test = $this->repository->findByNonexistent(1); + } + + public function findOneByDynamicType(): void + { + $test = $this->repository->findOneById(1); + + if ($test === null) { + throw new RuntimeException('Sorry, but no...'); + } + + $test->doSomething(); + $test->doSomethingElse(); + } + + public function findOneDynamicType2(): void + { + $test = $this->repository->findOneByNonexistent(1); + } + + public function countBy(): void + { + $test = $this->repository->countById(1); + if ($test === 'foo') { + + } + $test = $this->repository->countByNonexistent('test'); + } +} + +/** + * @ORM\Entity() + */ +class MyEntity +{ + /** + * @ORM\Id() + * @ORM\GeneratedValue() + * @ORM\Column(type="integer") + * + * @var int + */ + private $id; + + public function doSomething(): void + { + } +} + +interface EntityInterface +{ + +} + +class GetRepositoryOnNonClasses +{ + + public function doFoo(EntityManagerInterface $entityManager): void + { + $entityManager->getRepository(EntityInterface::class); + } + + public function doBar(EntityManagerInterface $entityManager): void + { + $entityManager->getRepository('nonexistentClass'); + } + +} + +abstract class BaseEntity +{ + + /** + * @return EntityRepository + */ + public function getRepository(): EntityRepository + { + return $this->getEntityManager()->getRepository(static::class); + } + + abstract public function getEntityManager(): EntityManager; + +} + +class Bug180Repository extends EntityRepository +{ + public const ALIAS = 'o'; + + + public function testingMethod(): int + { + $qb = $this->createQueryBuilder(); + + return (int) $qb->getQuery()->getSingleScalarResult(); + } +}