diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 476203bb..ad3633e9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.4.x" + - "2.0.x" jobs: lint: @@ -17,13 +17,12 @@ jobs: fail-fast: false matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" - "8.2" - "8.3" + - "8.4" steps: - name: "Checkout" @@ -40,9 +39,9 @@ jobs: - name: "Validate Composer" run: "composer validate" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^8.5.31 --no-update --update-with-dependencies" + - name: "Allow installing on PHP 8.4" + if: matrix.php-version == '8.4' + run: "composer config platform.php 8.3.99" - name: "Install dependencies" run: "composer install --no-interaction --no-progress" @@ -64,6 +63,7 @@ jobs: with: repository: "phpstan/build-cs" path: "build-cs" + ref: "2.x" - name: "Install PHP" uses: "shivammathur/setup-php@v2" @@ -97,12 +97,12 @@ jobs: fail-fast: false matrix: php-version: - - "7.2" - - "7.3" - "7.4" - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" dependencies: - "lowest" - "highest" @@ -112,8 +112,7 @@ jobs: - php-version: "8.3" dependencies: "highest" update-packages: | - composer config extra.patches.doctrine/orm --json --merge '["compatibility/patches/Base.patch", "compatibility/patches/Column.patch", "compatibility/patches/DateAddFunction.patch", "compatibility/patches/DateSubFunction.patch", "compatibility/patches/DiscriminatorColumn.patch", "compatibility/patches/DiscriminatorMap.patch", "compatibility/patches/Embeddable.patch", "compatibility/patches/Embedded.patch", "compatibility/patches/Entity.patch", "compatibility/patches/GeneratedValue.patch", "compatibility/patches/Id.patch", "compatibility/patches/InheritanceType.patch", "compatibility/patches/JoinColumn.patch", "compatibility/patches/JoinColumns.patch", "compatibility/patches/ManyToMany.patch", "compatibility/patches/ManyToOne.patch", "compatibility/patches/MappedSuperclass.patch", "compatibility/patches/OneToMany.patch", "compatibility/patches/OneToOne.patch", "compatibility/patches/OrderBy.patch", "compatibility/patches/UniqueConstraint.patch", "compatibility/patches/Version.patch"]' - composer config extra.patches.carbonphp/carbon-doctrine-types --json --merge '["compatibility/patches/DateTimeImmutableType.patch", "compatibility/patches/DateTimeType.patch"]' + composer config extra.patches.doctrine/orm --json --merge '["compatibility/patches/Column.patch", "compatibility/patches/DiscriminatorColumn.patch", "compatibility/patches/DiscriminatorMap.patch", "compatibility/patches/Embeddable.patch", "compatibility/patches/Embedded.patch", "compatibility/patches/Entity.patch", "compatibility/patches/GeneratedValue.patch", "compatibility/patches/Id.patch", "compatibility/patches/InheritanceType.patch", "compatibility/patches/JoinColumn.patch", "compatibility/patches/JoinColumns.patch", "compatibility/patches/ManyToMany.patch", "compatibility/patches/ManyToOne.patch", "compatibility/patches/MappedSuperclass.patch", "compatibility/patches/OneToMany.patch", "compatibility/patches/OneToOne.patch", "compatibility/patches/OrderBy.patch", "compatibility/patches/UniqueConstraint.patch", "compatibility/patches/Version.patch"]' composer require --dev doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 gedmo/doctrine-extensions:^3 -W steps: @@ -128,18 +127,14 @@ jobs: ini-file: development extensions: "mongodb" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^8.5.31 --no-update --update-with-dependencies" + - name: "Allow installing on PHP 8.4" + if: matrix.php-version == '8.4' + run: "composer config platform.php 8.3.99" - name: "Install lowest dependencies" if: ${{ matrix.dependencies == 'lowest' }} run: "composer update --prefer-lowest --no-interaction --no-progress" - - name: "Update Doctrine DBAl to ^3" - if: matrix.php-version != '7.2' && matrix.dependencies == 'lowest' - run: "composer require --dev doctrine/dbal:^3.3.8 --no-interaction --no-progress" - - name: "Install highest dependencies" if: ${{ matrix.dependencies == 'highest' }} run: "composer update --no-interaction --no-progress" @@ -158,12 +153,12 @@ jobs: fail-fast: false matrix: php-version: - - "7.3" - "7.4" - "8.0" - "8.1" - "8.2" - "8.3" + - "8.4" update-packages: - "" include: @@ -182,9 +177,9 @@ jobs: extensions: "mongodb" ini-file: development - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.2' || matrix.php-version == '7.3' - run: "composer require --dev phpunit/phpunit:^8.5.31 --no-update --update-with-dependencies" + - name: "Allow installing on PHP 8.4" + if: matrix.php-version == '8.4' + run: "composer config platform.php 8.3.99" - name: "Install dependencies" run: "composer update --no-interaction --no-progress" diff --git a/.github/workflows/platform-test.yml b/.github/workflows/platform-test.yml index 22e867ea..38110353 100644 --- a/.github/workflows/platform-test.yml +++ b/.github/workflows/platform-test.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "1.4.x" + - "2.0.x" jobs: tests: @@ -26,6 +26,7 @@ jobs: - "8.1" - "8.2" - "8.3" + - "8.4" update-packages: - "" include: @@ -35,6 +36,8 @@ jobs: update-packages: doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 gedmo/doctrine-extensions:^3 - php-version: "8.3" update-packages: doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 gedmo/doctrine-extensions:^3 + - php-version: "8.4" + update-packages: doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 gedmo/doctrine-extensions:^3 steps: - name: "Checkout" @@ -48,6 +51,10 @@ jobs: ini-file: development extensions: pdo, mysqli, pgsql, pdo_mysql, pdo_pgsql, pdo_sqlite, mongodb + - name: "Allow installing on PHP 8.4" + if: matrix.php-version == '8.4' + run: "composer config platform.php 8.3.99" + - name: "Install dependencies" run: "composer install --no-interaction --no-progress" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1a669a9..b8c96d48 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Generate changelog id: changelog - uses: metcalfc/changelog-generator@v4.3.1 + uses: metcalfc/changelog-generator@v4.6.2 with: myToken: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/test-projects.yml b/.github/workflows/test-projects.yml index 6bd1e05b..cebc3431 100644 --- a/.github/workflows/test-projects.yml +++ b/.github/workflows/test-projects.yml @@ -5,7 +5,7 @@ name: "Test projects" on: push: branches: - - "1.4.x" + - "2.0.x" jobs: test-projects: @@ -26,4 +26,4 @@ jobs: token: ${{ secrets.REPO_ACCESS_TOKEN }} repository: "${{ matrix.repository }}" event-type: test_phpstan - client-payload: '{"ref": "1.11.x"}' + client-payload: '{"ref": "2.1.x"}' diff --git a/LICENSE b/LICENSE index 7c0f2b7b..e5f34e60 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2016 Ondřej Mirtes +Copyright (c) 2025 PHPStan s.r.o. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 2ec6452c..efc169db 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ lint: .PHONY: cs-install cs-install: git clone https://github.com/phpstan/build-cs.git || true - git -C build-cs fetch origin && git -C build-cs reset --hard origin/main + git -C build-cs fetch origin && git -C build-cs reset --hard origin/2.x composer install --working-dir build-cs .PHONY: cs diff --git a/README.md b/README.md index 4f148e8f..c8a587b1 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ $query->getResult(); // array Queries are analyzed statically and do not require a running database server. This makes use of the Doctrine DQL parser and entities metadata. -Most DQL features are supported, including `GROUP BY`, `DISTINCT`, all flavors of `JOIN`, arithmetic expressions, functions, aggregations, `NEW`, etc. Sub queries and `INDEX BY` are not yet supported (infered type will be `mixed`). +Most DQL features are supported, including `GROUP BY`, `INDEX BY`, `DISTINCT`, all flavors of `JOIN`, arithmetic expressions, functions, aggregations, `NEW`, etc. Sub queries are not yet supported (infered type will be `mixed`). ### Query type inference of expressions @@ -286,3 +286,19 @@ class Floor extends FunctionNode implements TypedExpression } ``` + +## Literal strings + +Stub files in phpstan-doctrine come with many parameters marked with `literal-string`. This is a security-focused type that only allows literal strings written in code to be passed into these parameters. + +This reduces risk of SQL injection because dynamic strings from user input are not accepted in place of `literal-string`. + +An example where this type is used is `$sql` parameter in `Doctrine\Dbal\Connection::executeQuery()`. + +To enable this advanced type in phpstan-doctrine, use this configuration parameter: + +```neon +parameters: + doctrine: + literalString: true +``` diff --git a/compatibility/BackedEnum.stub b/compatibility/BackedEnum.stub new file mode 100644 index 00000000..2376a2fc --- /dev/null +++ b/compatibility/BackedEnum.stub @@ -0,0 +1,6 @@ +addMultiple($args); - } - diff --git a/compatibility/patches/DateAddFunction.patch b/compatibility/patches/DateAddFunction.patch deleted file mode 100644 index 79a7606a..00000000 --- a/compatibility/patches/DateAddFunction.patch +++ /dev/null @@ -1,10 +0,0 @@ ---- src/Query/AST/Functions/DateAddFunction.php 2024-02-09 14:22:59 -+++ src/Query/AST/Functions/DateAddFunction.php 2024-02-09 14:23:02 -@@ -71,7 +71,6 @@ - private function dispatchIntervalExpression(SqlWalker $sqlWalker): string - { - $sql = $this->intervalExpression->dispatch($sqlWalker); -- assert(is_numeric($sql)); - - return $sql; - } diff --git a/compatibility/patches/DateSubFunction.patch b/compatibility/patches/DateSubFunction.patch deleted file mode 100644 index 12a8fcf3..00000000 --- a/compatibility/patches/DateSubFunction.patch +++ /dev/null @@ -1,10 +0,0 @@ ---- src/Query/AST/Functions/DateSubFunction.php 2024-02-09 14:22:31 -+++ src/Query/AST/Functions/DateSubFunction.php 2024-02-09 14:22:50 -@@ -64,7 +64,6 @@ - private function dispatchIntervalExpression(SqlWalker $sqlWalker): string - { - $sql = $this->intervalExpression->dispatch($sqlWalker); -- assert(is_numeric($sql)); - - return $sql; - } diff --git a/compatibility/patches/DateTimeImmutableType.patch b/compatibility/patches/DateTimeImmutableType.patch deleted file mode 100644 index e8525247..00000000 --- a/compatibility/patches/DateTimeImmutableType.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- src/Carbon/Doctrine/DateTimeImmutableType.php 2023-12-10 16:33:53 -+++ src/Carbon/Doctrine/DateTimeImmutableType.php 2024-02-09 11:36:50 -@@ -17,7 +17,7 @@ - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -- public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTimeImmutable -+ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?CarbonImmutable - { - return $this->doConvertToPHPValue($value); - } diff --git a/compatibility/patches/DateTimeType.patch b/compatibility/patches/DateTimeType.patch deleted file mode 100644 index 0a36920f..00000000 --- a/compatibility/patches/DateTimeType.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- src/Carbon/Doctrine/DateTimeType.php 2023-12-10 16:33:53 -+++ src/Carbon/Doctrine/DateTimeType.php 2024-02-09 11:36:58 -@@ -17,7 +17,7 @@ - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ -- public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTime -+ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?Carbon - { - return $this->doConvertToPHPValue($value); - } diff --git a/composer.json b/composer.json index e992ccae..24ea4782 100644 --- a/composer.json +++ b/composer.json @@ -6,8 +6,8 @@ "MIT" ], "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11.7" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.13" }, "conflict": { "doctrine/collections": "<1.0", @@ -20,21 +20,21 @@ "cache/array-adapter": "^1.1", "composer/semver": "^3.3.2", "cweagans/composer-patches": "^1.7.3", - "doctrine/annotations": "^1.11 || ^2.0", + "doctrine/annotations": "^2.0", "doctrine/collections": "^1.6 || ^2.1", "doctrine/common": "^2.7 || ^3.0", - "doctrine/dbal": "^2.13.8 || ^3.3.3", + "doctrine/dbal": "^3.3.8", "doctrine/lexer": "^2.0 || ^3.0", - "doctrine/mongodb-odm": "^1.3 || ^2.4.3", + "doctrine/mongodb-odm": "^2.4.3", "doctrine/orm": "^2.16.0", "doctrine/persistence": "^2.2.1 || ^3.2", "gedmo/doctrine-extensions": "^3.8", "nesbot/carbon": "^2.49", - "nikic/php-parser": "^4.13.2", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.3.13", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^9.6.16", + "phpstan/phpstan-deprecation-rules": "^2.0.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6.20", "ramsey/uuid": "^4.2", "symfony/cache": "^5.4" }, diff --git a/extension.neon b/extension.neon index f4d3b7ef..93102dc0 100644 --- a/extension.neon +++ b/extension.neon @@ -1,22 +1,15 @@ parameters: doctrine: + reportDynamicQueryBuilders: false + reportUnknownTypes: false + allowNullablePropertyForRequiredField: false repositoryClass: null ormRepositoryClass: null odmRepositoryClass: null queryBuilderClass: null allCollectionsSelectable: true objectManagerLoader: null - searchOtherMethodsForQueryBuilderBeginning: true - queryBuilderFastAlgorithm: false - featureToggles: - skipCheckGenericClasses: - - Doctrine\ODM\MongoDB\Mapping\ClassMetadata - - Doctrine\ORM\Mapping\ClassMetadata - - Doctrine\ORM\Mapping\ClassMetadataInfo - - Doctrine\Persistence\Mapping\ClassMetadata - - Doctrine\ORM\AbstractQuery - - Doctrine\ORM\Query - - Doctrine\ORM\Tools\Pagination\Paginator + literalString: false stubFiles: - stubs/Criteria.stub - stubs/DBAL/Cache/CacheException.stub @@ -30,6 +23,7 @@ parameters: - stubs/EntityManager.stub - stubs/EntityManagerDecorator.stub - stubs/EntityManagerInterface.stub + - stubs/EntityRepository.stub - stubs/MongoClassMetadataInfo.stub - stubs/Persistence/ManagerRegistry.stub @@ -38,7 +32,6 @@ parameters: - stubs/Persistence/ObjectRepository.stub - stubs/RepositoryFactory.stub - stubs/Collections/ArrayCollection.stub - - stubs/Collections/ReadableCollection.stub - stubs/Collections/Selectable.stub - stubs/ORM/AbstractQuery.stub - stubs/ORM/Exception/ORMException.stub @@ -57,6 +50,7 @@ parameters: - stubs/ORM/UnexpectedResultException.stub - stubs/ORM/Query/Expr.stub - stubs/ORM/Query.stub + - stubs/ORM/QueryBuilder.stub - stubs/ORM/Query/Expr/Comparison.stub - stubs/ORM/Query/Expr/Composite.stub - stubs/ORM/Query/Expr/Func.stub @@ -73,8 +67,10 @@ parametersSchema: queryBuilderClass: schema(string(), nullable()) allCollectionsSelectable: bool() objectManagerLoader: schema(string(), nullable()) - searchOtherMethodsForQueryBuilderBeginning: bool() - queryBuilderFastAlgorithm: bool() + reportDynamicQueryBuilders: bool() + reportUnknownTypes: bool() + allowNullablePropertyForRequiredField: bool() + literalString: bool() ]) conditionalTags: @@ -107,7 +103,6 @@ services: class: PHPStan\Type\Doctrine\QueryBuilder\CreateQueryBuilderDynamicReturnTypeExtension arguments: queryBuilderClass: %doctrine.queryBuilderClass% - fasterVersion: %doctrine.queryBuilderFastAlgorithm% tags: - phpstan.broker.dynamicMethodReturnTypeExtension - @@ -184,7 +179,6 @@ services: - class: PHPStan\Type\Doctrine\QueryBuilder\OtherMethodQueryBuilderParser arguments: - descendIntoOtherMethods: %doctrine.searchOtherMethodsForQueryBuilderBeginning% parser: @defaultAnalysisParser - @@ -196,8 +190,6 @@ services: class: PHPStan\Stubs\Doctrine\StubFilesExtensionLoader tags: - phpstan.stubFilesExtension - arguments: - bleedingEdge: %featureToggles.bleedingEdge% doctrineQueryBuilderArgumentsProcessor: class: PHPStan\Type\Doctrine\ArgumentsProcessor @@ -305,6 +297,18 @@ services: class: PHPStan\Type\Doctrine\DBAL\QueryBuilder\QueryBuilderExecuteMethodExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Type\Doctrine\DBAL\RowCountMethodDynamicReturnTypeExtension + arguments: + class: Doctrine\DBAL\Result + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Type\Doctrine\DBAL\RowCountMethodDynamicReturnTypeExtension + arguments: + class: Doctrine\DBAL\Driver\Result + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension # Type descriptors - @@ -434,6 +438,13 @@ services: tags: - phpstan.phpDoc.typeNodeResolverExtension + - + class: PHPStan\PhpDoc\Doctrine\DoctrineLiteralStringTypeNodeResolverExtension + arguments: + enabled: %doctrine.literalString% + tags: + - phpstan.phpDoc.typeNodeResolverExtension + - class: PHPStan\Type\Doctrine\EntityManagerInterfaceThrowTypeExtension tags: diff --git a/phpstan-baseline-deprecations.neon b/phpstan-baseline-deprecations.neon new file mode 100644 index 00000000..bd96685e --- /dev/null +++ b/phpstan-baseline-deprecations.neon @@ -0,0 +1,127 @@ +parameters: + ignoreErrors: + - + message: ''' + #^Fetching class constant class of deprecated class Doctrine\\DBAL\\Types\\ArrayType\: + Use \{@link JsonType\} instead\.$# + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: src/Type/Doctrine/Descriptors/ArrayType.php + + - + message: ''' + #^Fetching class constant class of deprecated class Doctrine\\DBAL\\Types\\ObjectType\: + Use \{@link JsonType\} instead\.$# + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: src/Type/Doctrine/Descriptors/ObjectType.php + + - + message: ''' + #^Call to deprecated method getSchemaManager\(\) of class Doctrine\\DBAL\\Connection\: + Use \{@see createSchemaManager\(\)\} instead\.$# + ''' + identifier: method.deprecated + count: 1 + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + + - + message: ''' + #^Fetching class constant class of deprecated class Doctrine\\DBAL\\Types\\ArrayType\: + Use \{@link JsonType\} instead\.$# + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php + + - + message: ''' + #^Call to deprecated method create\(\) of class Doctrine\\ORM\\EntityManager\: + Use \{@see DriverManager\:\:getConnection\(\)\} to bootstrap the connection and call the constructor\.$# + ''' + identifier: staticMethod.deprecated + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + + - + message: ''' + #^Fetching class constant class of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Classes/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: tests/DoctrineIntegration/ORM/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Rules/DeadCode/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Rules/Properties/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Type/Doctrine/DBAL/mysqli.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Type/Doctrine/DBAL/pdo.php diff --git a/phpstan-baseline-orm-3.neon b/phpstan-baseline-orm-3.neon index 92e5f87c..ae5e8dfd 100644 --- a/phpstan-baseline-orm-3.neon +++ b/phpstan-baseline-orm-3.neon @@ -54,3 +54,102 @@ parameters: message: "#^Parameter \\#2 \\$className of static method Doctrine\\\\DBAL\\\\Types\\\\Type\\:\\:addType\\(\\) expects class\\-string\\, string given\\.$#" count: 1 path: tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php + + - + message: ''' + #^Fetching class constant class of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Classes/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: tests/DoctrineIntegration/ORM/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Rules/DeadCode/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Rules/Properties/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Type/Doctrine/DBAL/mysqli.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\. + Copyright \(c\) Doctrine Project + From https\://github\.com/doctrine/orm/blob/40fbbf4429b0d66517244051237a2bd0616a7a13/src/Mapping/Driver/AnnotationDriver\.php$# + ''' + identifier: new.deprecated + count: 1 + path: tests/Type/Doctrine/DBAL/pdo.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index cc1e2048..64b1a032 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,71 +1,241 @@ parameters: ignoreErrors: - - message: "#^Calling PHPStan\\\\Type\\\\ParserNodeTypeToPHPStanType\\:\\:resolve\\(\\) is not covered by backward compatibility promise\\. The method might change in a minor PHPStan version\\.$#" + message: '#^Call to internal method Doctrine\\DBAL\\Connection\:\:getParams\(\) from outside its root namespace Doctrine\.$#' + identifier: method.internal + count: 2 + path: src/Doctrine/Driver/DriverDetector.php + + - + message: ''' + #^Access to constant on deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: classConstant.deprecatedClass count: 1 - path: src/Rules/Doctrine/ORM/EntityColumnRule.php + path: src/Doctrine/Mapping/ClassMetadataFactory.php - - message: "#^Calling PHPStan\\\\Type\\\\TypehintHelper\\:\\:decideType\\(\\) is not covered by backward compatibility promise\\. The method might change in a minor PHPStan version\\.$#" + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass count: 1 - path: src/Rules/Doctrine/ORM/EntityColumnRule.php + path: src/Doctrine/Mapping/ClassMetadataFactory.php - - message: "#^Calling PHPStan\\\\Type\\\\ParserNodeTypeToPHPStanType\\:\\:resolve\\(\\) is not covered by backward compatibility promise\\. The method might change in a minor PHPStan version\\.$#" + message: '#^Calling PHPStan\\Type\\TypehintHelper\:\:decideType\(\) is not covered by backward compatibility promise\. The method might change in a minor PHPStan version\.$#' + identifier: phpstanApi.method count: 1 - path: src/Rules/Doctrine/ORM/EntityRelationRule.php + path: src/Rules/Doctrine/ORM/EntityColumnRule.php - - message: "#^Calling PHPStan\\\\Type\\\\TypehintHelper\\:\\:decideType\\(\\) is not covered by backward compatibility promise\\. The method might change in a minor PHPStan version\\.$#" + message: '#^Calling PHPStan\\Type\\TypehintHelper\:\:decideType\(\) is not covered by backward compatibility promise\. The method might change in a minor PHPStan version\.$#' + identifier: phpstanApi.method count: 1 path: src/Rules/Doctrine/ORM/EntityRelationRule.php - - message: "#^PHPDoc tag @var with type class\\-string is not subtype of native type 'Doctrine\\\\\\\\Bundle…'\\.$#" + message: '#^PHPDoc tag @var with type class\-string is not subtype of native type ''Doctrine\\\\Bundle…''\.$#' + identifier: varTag.nativeType count: 1 path: src/Stubs/Doctrine/StubFilesExtensionLoader.php - - message: "#^Accessing PHPStan\\\\Rules\\\\Classes\\\\InstantiationRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: ''' + #^Catching deprecated class Doctrine\\Common\\CommonException\: + The doctrine/common package is deprecated, please use specific packages and their exceptions instead\.$# + ''' + identifier: catch.deprecatedClass + count: 1 + path: src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php + + - + message: ''' + #^Catching deprecated class Doctrine\\ORM\\ORMException\: + Use Doctrine\\ORM\\Exception\\ORMException for catch and instanceof$# + ''' + identifier: catch.deprecatedClass + count: 1 + path: src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php + + - + message: ''' + #^Access to constant on deprecated class Doctrine\\DBAL\\Types\\ArrayType\: + Use \{@link JsonType\} instead\.$# + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: src/Type/Doctrine/Descriptors/ArrayType.php + + - + message: ''' + #^Access to constant on deprecated class Doctrine\\DBAL\\Types\\ObjectType\: + Use \{@link JsonType\} instead\.$# + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: src/Type/Doctrine/Descriptors/ObjectType.php + + - + message: '#^Parameter \$condExpr of method PHPStan\\Type\\Doctrine\\Query\\QueryResultTypeWalker\:\:walkConditionalExpression\(\) has typehint with internal interface Doctrine\\ORM\\Query\\AST\\Phase2OptimizableConditional\.$#' + identifier: parameter.internalInterface + count: 1 + path: src/Type/Doctrine/Query/QueryResultTypeWalker.php + + - + message: '#^Parameter \$condExpr of method PHPStan\\Type\\Doctrine\\Query\\QueryResultTypeWalker\:\:walkJoinAssociationDeclaration\(\) has typehint with internal interface Doctrine\\ORM\\Query\\AST\\Phase2OptimizableConditional\.$#' + identifier: parameter.internalInterface + count: 1 + path: src/Type/Doctrine/Query/QueryResultTypeWalker.php + + - + message: ''' + #^Catching deprecated class Doctrine\\Common\\CommonException\: + The doctrine/common package is deprecated, please use specific packages and their exceptions instead\.$# + ''' + identifier: catch.deprecatedClass + count: 1 + path: src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php + + - + message: ''' + #^Catching deprecated class Doctrine\\ORM\\ORMException\: + Use Doctrine\\ORM\\Exception\\ORMException for catch and instanceof$# + ''' + identifier: catch.deprecatedClass + count: 1 + path: src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php + + - + message: '#^Accessing PHPStan\\Rules\\Classes\\InstantiationRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\DeadCode\\\\UnusedPrivatePropertyRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Classes/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/DoctrineIntegration/ORM/entity-manager.php + + - + message: '#^Call to internal method PHPUnit\\Framework\\TestCase\:\:dataName\(\) from outside its root namespace PHPUnit\.$#' + identifier: method.internal + count: 14 + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + + - + message: '#^Accessing PHPStan\\Rules\\DeadCode\\UnusedPrivatePropertyRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\Methods\\\\CallMethodsRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Rules/DeadCode/entity-manager.php + + - + message: ''' + #^Access to constant on deprecated class Doctrine\\DBAL\\Types\\ArrayType\: + Use \{@link JsonType\} instead\.$# + ''' + identifier: classConstant.deprecatedClass + count: 1 + path: tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php + + - + message: '#^Accessing PHPStan\\Rules\\Methods\\CallMethodsRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/Doctrine/ORM/MagicRepositoryMethodCallRuleTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\Exceptions\\\\CatchWithUnthrownExceptionRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Accessing PHPStan\\Rules\\Exceptions\\CatchWithUnthrownExceptionRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant 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\\.$#" + message: '#^Accessing PHPStan\\Rules\\Exceptions\\TooWideMethodThrowTypeRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant 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\\.$#" + message: '#^Accessing PHPStan\\Rules\\DeadCode\\UnusedPrivatePropertyRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\DeadCode\\\\UnusedPrivatePropertyRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Accessing PHPStan\\Rules\\DeadCode\\UnusedPrivatePropertyRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/Properties/MissingGedmoPropertyAssignRuleTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\Properties\\\\MissingReadOnlyByPhpDocPropertyAssignRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Accessing PHPStan\\Rules\\Properties\\MissingReadOnlyByPhpDocPropertyAssignRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php - - message: "#^Accessing PHPStan\\\\Rules\\\\Properties\\\\MissingReadOnlyPropertyAssignRule\\:\\:class is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#" + message: '#^Accessing PHPStan\\Rules\\Properties\\MissingReadOnlyPropertyAssignRule\:\:class is not covered by backward compatibility promise\. The class might change in a minor PHPStan version\.$#' + identifier: phpstanApi.classConstant count: 1 path: tests/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Rules/Properties/entity-manager.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Type/Doctrine/DBAL/mysqli.php + + - + message: ''' + #^Instantiation of deprecated class Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver\: + This class will be removed in 3\.0 without replacement\.$# + ''' + identifier: new.deprecatedClass + count: 1 + path: tests/Type/Doctrine/DBAL/pdo.php diff --git a/phpstan.neon b/phpstan.neon index d099cb21..efbf455d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,9 +2,11 @@ includes: - extension.neon - rules.neon - phpstan-baseline.neon + - phpstan-baseline-deprecations.neon - phpstan-baseline-dbal-3.neon - compatibility/orm-3-baseline.php - vendor/phpstan/phpstan-strict-rules/rules.neon + - vendor/phpstan/phpstan-deprecation-rules/rules.neon - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon - phar://phpstan.phar/conf/bleedingEdge.neon @@ -64,3 +66,9 @@ parameters: - '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''createSchemaManager'' will always evaluate to true\.$#' - '#^Call to an undefined method Doctrine\\DBAL\\Connection\:\:getSchemaManager\(\)\.$#' path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php + +services: + - + class: PHPStan\BackedEnumStubExtension + tags: + - phpstan.stubFilesExtension diff --git a/rules.neon b/rules.neon index feb41184..0853184d 100644 --- a/rules.neon +++ b/rules.neon @@ -12,24 +12,19 @@ parametersSchema: queryBuilderClass: schema(string(), nullable()) allCollectionsSelectable: bool() objectManagerLoader: schema(string(), nullable()) - searchOtherMethodsForQueryBuilderBeginning: bool() - queryBuilderFastAlgorithm: bool() reportDynamicQueryBuilders: bool() reportUnknownTypes: bool() allowNullablePropertyForRequiredField: bool() + literalString: bool() ]) rules: - PHPStan\Rules\Doctrine\ORM\DqlRule - PHPStan\Rules\Doctrine\ORM\RepositoryMethodCallRule + - PHPStan\Rules\Doctrine\ORM\EntityConstructorNotFinalRule + - PHPStan\Rules\Doctrine\ORM\EntityMappingExceptionRule - PHPStan\Rules\Doctrine\ORM\EntityNotFinalRule -conditionalTags: - PHPStan\Rules\Doctrine\ORM\EntityMappingExceptionRule: - phpstan.rules.rule: %featureToggles.bleedingEdge% - PHPStan\Rules\Doctrine\ORM\EntityConstructorNotFinalRule: - phpstan.rules.rule: %featureToggles.bleedingEdge% - services: - class: PHPStan\Rules\Doctrine\ORM\QueryBuilderDqlRule @@ -42,23 +37,15 @@ services: arguments: reportUnknownTypes: %doctrine.reportUnknownTypes% allowNullablePropertyForRequiredField: %doctrine.allowNullablePropertyForRequiredField% - bleedingEdge: %featureToggles.bleedingEdge% descriptorRegistry: @doctrineTypeDescriptorRegistry tags: - phpstan.rules.rule - - - class: PHPStan\Rules\Doctrine\ORM\EntityMappingExceptionRule - - - class: PHPStan\Rules\Doctrine\ORM\EntityNotFinalRule - class: PHPStan\Rules\Doctrine\ORM\EntityRelationRule arguments: allowNullablePropertyForRequiredField: %doctrine.allowNullablePropertyForRequiredField% - bleedingEdge: %featureToggles.bleedingEdge% tags: - phpstan.rules.rule - - - class: PHPStan\Rules\Doctrine\ORM\EntityConstructorNotFinalRule - class: PHPStan\Classes\DoctrineProxyForbiddenClassNamesExtension tags: diff --git a/src/Classes/DoctrineProxyForbiddenClassNamesExtension.php b/src/Classes/DoctrineProxyForbiddenClassNamesExtension.php index 1ff7f4f6..dbca26af 100644 --- a/src/Classes/DoctrineProxyForbiddenClassNamesExtension.php +++ b/src/Classes/DoctrineProxyForbiddenClassNamesExtension.php @@ -8,8 +8,7 @@ class DoctrineProxyForbiddenClassNamesExtension implements ForbiddenClassNameExtension { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { diff --git a/src/Doctrine/DoctrineDiagnoseExtension.php b/src/Doctrine/DoctrineDiagnoseExtension.php index 58fda398..0502c736 100644 --- a/src/Doctrine/DoctrineDiagnoseExtension.php +++ b/src/Doctrine/DoctrineDiagnoseExtension.php @@ -15,11 +15,9 @@ class DoctrineDiagnoseExtension implements DiagnoseExtension { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var DriverDetector */ - private $driverDetector; + private DriverDetector $driverDetector; public function __construct( ObjectMetadataResolver $objectMetadataResolver, @@ -34,7 +32,7 @@ public function print(Output $output): void { $output->writeLineFormatted(sprintf( 'Doctrine\'s objectManagerLoader: %s', - $this->objectMetadataResolver->hasObjectManagerLoader() ? 'In use' : 'No' + $this->objectMetadataResolver->hasObjectManagerLoader() ? 'In use' : 'No', )); $objectManager = $this->objectMetadataResolver->getObjectManager(); @@ -44,7 +42,7 @@ public function print(Output $output): void $output->writeLineFormatted(sprintf( 'Detected driver: %s', - $driver === null ? 'None' : $driver + $driver === null ? 'None' : $driver, )); } diff --git a/src/Doctrine/Mapping/ClassMetadataFactory.php b/src/Doctrine/Mapping/ClassMetadataFactory.php index b2f82388..764268f1 100644 --- a/src/Doctrine/Mapping/ClassMetadataFactory.php +++ b/src/Doctrine/Mapping/ClassMetadataFactory.php @@ -18,8 +18,7 @@ class ClassMetadataFactory extends \Doctrine\ORM\Mapping\ClassMetadataFactory { - /** @var string */ - private $tmpDir; + private string $tmpDir; public function __construct(string $tmpDir) { diff --git a/src/Doctrine/Mapping/MappingDriverChain.php b/src/Doctrine/Mapping/MappingDriverChain.php index ebcdc423..532b208b 100644 --- a/src/Doctrine/Mapping/MappingDriverChain.php +++ b/src/Doctrine/Mapping/MappingDriverChain.php @@ -11,7 +11,7 @@ class MappingDriverChain implements MappingDriver { /** @var MappingDriver[] */ - private $drivers; + private array $drivers; /** * @param MappingDriver[] $drivers diff --git a/src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php b/src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php new file mode 100644 index 00000000..b034f57a --- /dev/null +++ b/src/PhpDoc/Doctrine/DoctrineLiteralStringTypeNodeResolverExtension.php @@ -0,0 +1,44 @@ +enabled = $enabled; + } + + public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type + { + if (!$typeNode instanceof IdentifierTypeNode) { + return null; + } + + if ($typeNode->name !== '__doctrine-literal-string') { + return null; + } + + if ($this->enabled) { + return new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]); + } + + return new StringType(); + } + +} diff --git a/src/PhpDoc/Doctrine/QueryTypeNodeResolverExtension.php b/src/PhpDoc/Doctrine/QueryTypeNodeResolverExtension.php index 39cd65d9..c8193892 100644 --- a/src/PhpDoc/Doctrine/QueryTypeNodeResolverExtension.php +++ b/src/PhpDoc/Doctrine/QueryTypeNodeResolverExtension.php @@ -18,8 +18,7 @@ class QueryTypeNodeResolverExtension implements TypeNodeResolverExtension, TypeNodeResolverAwareExtension { - /** @var TypeNodeResolver */ - private $typeNodeResolver; + private TypeNodeResolver $typeNodeResolver; public function setTypeNodeResolver(TypeNodeResolver $typeNodeResolver): void { @@ -47,7 +46,7 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type [ new NullType(), $this->typeNodeResolver->resolve($typeNode->genericTypes[0], $nameScope), - ] + ], ); } diff --git a/src/Reflection/Doctrine/DoctrineSelectableClassReflectionExtension.php b/src/Reflection/Doctrine/DoctrineSelectableClassReflectionExtension.php index 16970a60..ca726d8c 100644 --- a/src/Reflection/Doctrine/DoctrineSelectableClassReflectionExtension.php +++ b/src/Reflection/Doctrine/DoctrineSelectableClassReflectionExtension.php @@ -10,8 +10,7 @@ class DoctrineSelectableClassReflectionExtension implements MethodsClassReflectionExtension { - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; public function __construct(ReflectionProvider $reflectionProvider) { diff --git a/src/Reflection/Doctrine/DummyParameter.php b/src/Reflection/Doctrine/DummyParameter.php index 725f29f6..00eefa2c 100644 --- a/src/Reflection/Doctrine/DummyParameter.php +++ b/src/Reflection/Doctrine/DummyParameter.php @@ -9,23 +9,17 @@ class DummyParameter implements ParameterReflection { - /** @var string */ - private $name; + private string $name; - /** @var Type */ - private $type; + private Type $type; - /** @var bool */ - private $optional; + private bool $optional; - /** @var PassedByReference */ - private $passedByReference; + private PassedByReference $passedByReference; - /** @var bool */ - private $variadic; + private bool $variadic; - /** @var Type|null */ - private $defaultValue; + private ?Type $defaultValue = null; public function __construct(string $name, Type $type, bool $optional, ?PassedByReference $passedByReference, bool $variadic, ?Type $defaultValue) { diff --git a/src/Reflection/Doctrine/EntityRepositoryClassReflectionExtension.php b/src/Reflection/Doctrine/EntityRepositoryClassReflectionExtension.php index 799349da..52a8dcff 100644 --- a/src/Reflection/Doctrine/EntityRepositoryClassReflectionExtension.php +++ b/src/Reflection/Doctrine/EntityRepositoryClassReflectionExtension.php @@ -22,8 +22,7 @@ class EntityRepositoryClassReflectionExtension implements MethodsClassReflectionExtension { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { diff --git a/src/Reflection/Doctrine/MagicRepositoryMethodReflection.php b/src/Reflection/Doctrine/MagicRepositoryMethodReflection.php index 125f1440..94dc6011 100644 --- a/src/Reflection/Doctrine/MagicRepositoryMethodReflection.php +++ b/src/Reflection/Doctrine/MagicRepositoryMethodReflection.php @@ -21,14 +21,11 @@ class MagicRepositoryMethodReflection implements MethodReflection { - /** @var ClassReflection */ - private $declaringClass; + private ClassReflection $declaringClass; - /** @var string */ - private $name; + private string $name; - /** @var Type */ - private $type; + private Type $type; public function __construct( ClassReflection $declaringClass, @@ -104,7 +101,7 @@ public function getVariants(): array null, $arguments, false, - $this->type + $this->type, ), ]; } diff --git a/src/Rules/Doctrine/ORM/DqlRule.php b/src/Rules/Doctrine/ORM/DqlRule.php index 7a121525..23f4da41 100644 --- a/src/Rules/Doctrine/ORM/DqlRule.php +++ b/src/Rules/Doctrine/ORM/DqlRule.php @@ -11,7 +11,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\ObjectType; -use PHPStan\Type\TypeUtils; use function count; use function sprintf; @@ -21,8 +20,7 @@ class DqlRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { @@ -55,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $dqls = TypeUtils::getConstantStrings($scope->getType($node->getArgs()[0]->value)); + $dqls = $scope->getType($node->getArgs()[0]->value)->getConstantStrings(); if (count($dqls) === 0) { return []; } diff --git a/src/Rules/Doctrine/ORM/EntityColumnRule.php b/src/Rules/Doctrine/ORM/EntityColumnRule.php index e51e9c95..b5f42bb6 100644 --- a/src/Rules/Doctrine/ORM/EntityColumnRule.php +++ b/src/Rules/Doctrine/ORM/EntityColumnRule.php @@ -16,11 +16,11 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; -use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use Throwable; use function get_class; @@ -33,31 +33,22 @@ class EntityColumnRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var DescriptorRegistry */ - private $descriptorRegistry; + private DescriptorRegistry $descriptorRegistry; - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; - /** @var bool */ - private $reportUnknownTypes; + private bool $reportUnknownTypes; - /** @var bool */ - private $allowNullablePropertyForRequiredField; - - /** @var bool */ - private $bleedingEdge; + private bool $allowNullablePropertyForRequiredField; public function __construct( ObjectMetadataResolver $objectMetadataResolver, DescriptorRegistry $descriptorRegistry, ReflectionProvider $reflectionProvider, bool $reportUnknownTypes, - bool $allowNullablePropertyForRequiredField, - bool $bleedingEdge + bool $allowNullablePropertyForRequiredField ) { $this->objectMetadataResolver = $objectMetadataResolver; @@ -65,7 +56,6 @@ public function __construct( $this->reflectionProvider = $reflectionProvider; $this->reportUnknownTypes = $reportUnknownTypes; $this->allowNullablePropertyForRequiredField = $allowNullablePropertyForRequiredField; - $this->bleedingEdge = $bleedingEdge; } public function getNodeType(): string @@ -75,9 +65,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$this->bleedingEdge && !$this->objectMetadataResolver->hasObjectManagerLoader()) { - return []; - } $class = $scope->getClassReflection(); if ($class === null) { return []; @@ -105,7 +92,7 @@ public function processNode(Node $node, Scope $scope): array 'Property %s::$%s: Doctrine type "%s" does not have any registered descriptor.', $className, $propertyName, - $fieldMapping['type'] + $fieldMapping['type'], ))->identifier('doctrine.descriptorNotFound')->build(), ] : []; } @@ -115,25 +102,58 @@ public function processNode(Node $node, Scope $scope): array $enumTypeString = $fieldMapping['enumType'] ?? null; if ($enumTypeString !== null) { - if ($this->reflectionProvider->hasClass($enumTypeString)) { - $enumReflection = $this->reflectionProvider->getClass($enumTypeString); - $backedEnumType = $enumReflection->getBackedEnumType(); - if ($backedEnumType !== null) { - if (!$backedEnumType->equals($writableToDatabaseType) || !$backedEnumType->equals($writableToPropertyType)) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'Property %s::$%s type mapping mismatch: backing type %s of enum %s does not match database type %s.', - $className, - $propertyName, - $backedEnumType->describe(VerbosityLevel::typeOnly()), - $enumReflection->getDisplayName(), - $writableToDatabaseType->describe(VerbosityLevel::typeOnly()) - ))->identifier('doctrine.enumType')->build(); + if ($writableToDatabaseType->isArray()->no() && $writableToPropertyType->isArray()->no()) { + if ($this->reflectionProvider->hasClass($enumTypeString)) { + $enumReflection = $this->reflectionProvider->getClass($enumTypeString); + $backedEnumType = $enumReflection->getBackedEnumType(); + if ($backedEnumType !== null) { + if (!$backedEnumType->equals($writableToDatabaseType) || !$backedEnumType->equals($writableToPropertyType)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s type mapping mismatch: backing type %s of enum %s does not match database type %s.', + $className, + $propertyName, + $backedEnumType->describe(VerbosityLevel::typeOnly()), + $enumReflection->getDisplayName(), + $writableToDatabaseType->describe(VerbosityLevel::typeOnly()), + ))->identifier('doctrine.enumType')->build(); + } } } + $enumType = new ObjectType($enumTypeString); + $writableToPropertyType = $enumType; + $writableToDatabaseType = $enumType; + } else { + $enumType = new ObjectType($enumTypeString); + if ($this->reflectionProvider->hasClass($enumTypeString)) { + $enumReflection = $this->reflectionProvider->getClass($enumTypeString); + $backedEnumType = $enumReflection->getBackedEnumType(); + if ($backedEnumType !== null) { + if (!$backedEnumType->equals($writableToDatabaseType->getIterableValueType()) || !$backedEnumType->equals($writableToPropertyType->getIterableValueType())) { + $errors[] = RuleErrorBuilder::message( + sprintf( + 'Property %s::$%s type mapping mismatch: backing type %s of enum %s does not match value type %s of the database type %s.', + $className, + $propertyName, + $backedEnumType->describe(VerbosityLevel::typeOnly()), + $enumReflection->getDisplayName(), + $writableToDatabaseType->getIterableValueType()->describe(VerbosityLevel::typeOnly()), + $writableToDatabaseType->describe(VerbosityLevel::typeOnly()), + ), + )->identifier('doctrine.enumType')->build(); + } + } + } + + $writableToPropertyType = TypeCombinator::intersect(new ArrayType( + $writableToPropertyType->getIterableKeyType(), + $enumType, + ), ...TypeUtils::getAccessoryTypes($writableToPropertyType)); + $writableToDatabaseType = TypeCombinator::intersect(new ArrayType( + $writableToDatabaseType->getIterableKeyType(), + $enumType, + ), ...TypeUtils::getAccessoryTypes($writableToDatabaseType)); + } - $enumType = new ObjectType($enumTypeString); - $writableToPropertyType = $enumType; - $writableToDatabaseType = $enumType; } $identifiers = []; @@ -155,7 +175,7 @@ public function processNode(Node $node, Scope $scope): array } $phpDocType = $node->getPhpDocType(); - $nativeType = $node->getNativeType() !== null ? ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $scope->getClassReflection()) : new MixedType(); + $nativeType = $node->getNativeType() ?? new MixedType(); $propertyType = TypehintHelper::decideType($nativeType, $phpDocType); if (get_class($propertyType) === MixedType::class || $propertyType instanceof ErrorType || $propertyType instanceof NeverType) { @@ -177,7 +197,7 @@ public function processNode(Node $node, Scope $scope): array $className, $propertyName, $writableToPropertyType->describe(VerbosityLevel::getRecommendedLevelByType($propertyTransformedType, $writableToPropertyType)), - $propertyType->describe(VerbosityLevel::getRecommendedLevelByType($propertyTransformedType, $writableToPropertyType)) + $propertyType->describe(VerbosityLevel::getRecommendedLevelByType($propertyTransformedType, $writableToPropertyType)), ))->identifier('doctrine.columnType')->build(); } @@ -185,7 +205,7 @@ public function processNode(Node $node, Scope $scope): array !$writableToDatabaseType->isSuperTypeOf( $this->allowNullablePropertyForRequiredField || (in_array($propertyName, $identifiers, true) && !$nullable) ? TypeCombinator::removeNull($propertyType) - : $propertyType + : $propertyType, )->yes() ) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -193,7 +213,7 @@ public function processNode(Node $node, Scope $scope): array $className, $propertyName, $propertyTransformedType->describe(VerbosityLevel::getRecommendedLevelByType($writableToDatabaseType, $propertyType)), - $writableToDatabaseType->describe(VerbosityLevel::getRecommendedLevelByType($writableToDatabaseType, $propertyType)) + $writableToDatabaseType->describe(VerbosityLevel::getRecommendedLevelByType($writableToDatabaseType, $propertyType)), ))->identifier('doctrine.columnType')->build(); } return $errors; diff --git a/src/Rules/Doctrine/ORM/EntityConstructorNotFinalRule.php b/src/Rules/Doctrine/ORM/EntityConstructorNotFinalRule.php index 5fd01213..24876b38 100644 --- a/src/Rules/Doctrine/ORM/EntityConstructorNotFinalRule.php +++ b/src/Rules/Doctrine/ORM/EntityConstructorNotFinalRule.php @@ -17,8 +17,7 @@ class EntityConstructorNotFinalRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { @@ -57,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'Constructor of class %s is final which can cause problems with proxies.', - $classReflection->getDisplayName() + $classReflection->getDisplayName(), ))->identifier('doctrine.finalConstructor')->build(), ]; } diff --git a/src/Rules/Doctrine/ORM/EntityMappingExceptionRule.php b/src/Rules/Doctrine/ORM/EntityMappingExceptionRule.php index d74019ba..d0ce97f7 100644 --- a/src/Rules/Doctrine/ORM/EntityMappingExceptionRule.php +++ b/src/Rules/Doctrine/ORM/EntityMappingExceptionRule.php @@ -18,8 +18,7 @@ class EntityMappingExceptionRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct( ObjectMetadataResolver $objectMetadataResolver diff --git a/src/Rules/Doctrine/ORM/EntityNotFinalRule.php b/src/Rules/Doctrine/ORM/EntityNotFinalRule.php index b4dfe4c7..9dd39ff3 100644 --- a/src/Rules/Doctrine/ORM/EntityNotFinalRule.php +++ b/src/Rules/Doctrine/ORM/EntityNotFinalRule.php @@ -17,8 +17,7 @@ class EntityNotFinalRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { @@ -52,7 +51,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'Entity class %s is final which can cause problems with proxies.', - $classReflection->getDisplayName() + $classReflection->getDisplayName(), ))->identifier('doctrine.finalEntity')->build(), ]; } diff --git a/src/Rules/Doctrine/ORM/EntityRelationRule.php b/src/Rules/Doctrine/ORM/EntityRelationRule.php index 69e0c2a5..84ce9120 100644 --- a/src/Rules/Doctrine/ORM/EntityRelationRule.php +++ b/src/Rules/Doctrine/ORM/EntityRelationRule.php @@ -13,7 +13,6 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; -use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; use PHPStan\Type\VerbosityLevel; @@ -28,24 +27,17 @@ class EntityRelationRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var bool */ - private $allowNullablePropertyForRequiredField; - - /** @var bool */ - private $bleedingEdge; + private bool $allowNullablePropertyForRequiredField; public function __construct( ObjectMetadataResolver $objectMetadataResolver, - bool $allowNullablePropertyForRequiredField, - bool $bleedingEdge + bool $allowNullablePropertyForRequiredField ) { $this->objectMetadataResolver = $objectMetadataResolver; $this->allowNullablePropertyForRequiredField = $allowNullablePropertyForRequiredField; - $this->bleedingEdge = $bleedingEdge; } public function getNodeType(): string @@ -55,10 +47,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$this->bleedingEdge && !$this->objectMetadataResolver->hasObjectManagerLoader()) { - return []; - } - $class = $scope->getClassReflection(); if ($class === null) { return []; @@ -102,12 +90,12 @@ public function processNode(Node $node, Scope $scope): array $toMany = true; $columnType = TypeCombinator::intersect( new ObjectType('Doctrine\Common\Collections\Collection'), - new IterableType(new MixedType(), new ObjectType($associationMapping['targetEntity'])) + new IterableType(new MixedType(), new ObjectType($associationMapping['targetEntity'])), ); } $phpDocType = $node->getPhpDocType(); - $nativeType = $node->getNativeType() !== null ? ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $scope->getClassReflection()) : new MixedType(); + $nativeType = $node->getNativeType() ?? new MixedType(); $propertyType = TypehintHelper::decideType($nativeType, $phpDocType); $errors = []; @@ -125,7 +113,7 @@ public function processNode(Node $node, Scope $scope): array ) { $propertyTypeToCheckAgainst = TypeCombinator::intersect( $collectionObjectType, - new IterableType(new MixedType(true), $propertyType->getIterableValueType()) + new IterableType(new MixedType(true), $propertyType->getIterableValueType()), ); } if (!$propertyTypeToCheckAgainst->isSuperTypeOf($columnType)->yes()) { @@ -134,14 +122,14 @@ public function processNode(Node $node, Scope $scope): array $className, $propertyName, $columnType->describe(VerbosityLevel::typeOnly()), - $propertyType->describe(VerbosityLevel::typeOnly()) + $propertyType->describe(VerbosityLevel::typeOnly()), ))->identifier('doctrine.associationType')->build(); } if ( !$columnType->isSuperTypeOf( $this->allowNullablePropertyForRequiredField ? TypeCombinator::removeNull($propertyType) - : $propertyType + : $propertyType, )->yes() ) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -149,7 +137,7 @@ public function processNode(Node $node, Scope $scope): array $className, $propertyName, $propertyType->describe(VerbosityLevel::typeOnly()), - $columnType->describe(VerbosityLevel::typeOnly()) + $columnType->describe(VerbosityLevel::typeOnly()), ))->identifier('doctrine.associationType')->build(); } } diff --git a/src/Rules/Doctrine/ORM/PropertiesExtension.php b/src/Rules/Doctrine/ORM/PropertiesExtension.php index 4fdbf57d..9eab7dd7 100644 --- a/src/Rules/Doctrine/ORM/PropertiesExtension.php +++ b/src/Rules/Doctrine/ORM/PropertiesExtension.php @@ -12,8 +12,7 @@ class PropertiesExtension implements ReadWritePropertiesExtension { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { diff --git a/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php b/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php index 8596810d..69f9791a 100644 --- a/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php +++ b/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php @@ -13,7 +13,6 @@ use PHPStan\Type\Doctrine\DoctrineTypeUtils; use PHPStan\Type\Doctrine\ObjectMetadataResolver; use PHPStan\Type\ObjectType; -use PHPStan\Type\TypeUtils; use Throwable; use function array_values; use function count; @@ -26,11 +25,9 @@ class QueryBuilderDqlRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var bool */ - private $reportDynamicQueryBuilders; + private bool $reportDynamicQueryBuilders; public function __construct( ObjectMetadataResolver $objectMetadataResolver, @@ -83,7 +80,7 @@ public function processNode(Node $node, Scope $scope): array ]; } - $dqls = TypeUtils::getConstantStrings($dqlType); + $dqls = $dqlType->getConstantStrings(); if (count($dqls) === 0) { if ($this->reportDynamicQueryBuilders) { return [ diff --git a/src/Rules/Doctrine/ORM/RepositoryMethodCallRule.php b/src/Rules/Doctrine/ORM/RepositoryMethodCallRule.php index 9061d6c4..9f5231fe 100644 --- a/src/Rules/Doctrine/ORM/RepositoryMethodCallRule.php +++ b/src/Rules/Doctrine/ORM/RepositoryMethodCallRule.php @@ -19,8 +19,7 @@ class RepositoryMethodCallRule implements Rule { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { @@ -82,7 +81,7 @@ public function processNode(Node $node, Scope $scope): array $calledOnType->describe(VerbosityLevel::typeOnly()), $methodName, $entityClassNames[0], - $fieldName->getValue() + $fieldName->getValue(), ))->identifier(sprintf('doctrine.%sArgument', $methodName))->build(); } } diff --git a/src/Rules/Gedmo/PropertiesExtension.php b/src/Rules/Gedmo/PropertiesExtension.php index d43b4308..e82f3bc5 100644 --- a/src/Rules/Gedmo/PropertiesExtension.php +++ b/src/Rules/Gedmo/PropertiesExtension.php @@ -41,11 +41,9 @@ class PropertiesExtension implements ReadWritePropertiesExtension Gedmo\Language::class, ]; - /** @var AnnotationReader|null */ - private $annotationReader; + private ?AnnotationReader $annotationReader = null; - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { diff --git a/src/Stubs/Doctrine/StubFilesExtensionLoader.php b/src/Stubs/Doctrine/StubFilesExtensionLoader.php index 95d43a66..fc4b48aa 100644 --- a/src/Stubs/Doctrine/StubFilesExtensionLoader.php +++ b/src/Stubs/Doctrine/StubFilesExtensionLoader.php @@ -9,49 +9,33 @@ use PHPStan\PhpDoc\StubFilesExtension; use function class_exists; use function dirname; -use function file_exists; use function strpos; class StubFilesExtensionLoader implements StubFilesExtension { - /** @var Reflector */ - private $reflector; - - /** @var bool */ - private $bleedingEdge; + private Reflector $reflector; public function __construct( - Reflector $reflector, - bool $bleedingEdge + Reflector $reflector ) { $this->reflector = $reflector; - $this->bleedingEdge = $bleedingEdge; } public function getFiles(): array { $stubsDir = dirname(dirname(dirname(__DIR__))) . '/stubs'; - $path = $stubsDir; - - if ($this->bleedingEdge === true) { - $path .= '/bleedingEdge'; - } - $files = []; - if (file_exists($path . '/DBAL/Connection4.stub') && $this->isInstalledVersion('doctrine/dbal', 4)) { - $files[] = $path . '/DBAL/Connection4.stub'; - $files[] = $path . '/DBAL/ArrayParameterType.stub'; - $files[] = $path . '/DBAL/ParameterType.stub'; + if ($this->isInstalledVersion('doctrine/dbal', 4)) { + $files[] = $stubsDir . '/DBAL/Connection4.stub'; + $files[] = $stubsDir . '/DBAL/ArrayParameterType.stub'; + $files[] = $stubsDir . '/DBAL/ParameterType.stub'; } else { - $files[] = $path . '/DBAL/Connection.stub'; + $files[] = $stubsDir . '/DBAL/Connection.stub'; } - $files[] = $path . '/ORM/QueryBuilder.stub'; - $files[] = $path . '/EntityRepository.stub'; - $hasLazyServiceEntityRepositoryAsParent = false; try { @@ -79,8 +63,10 @@ public function getFiles(): array $collectionVersion = null; } if ($collectionVersion !== null && strpos($collectionVersion, '1.') === 0) { + $files[] = $stubsDir . '/Collections/ReadableCollection1.stub'; $files[] = $stubsDir . '/Collections/Collection1.stub'; } else { + $files[] = $stubsDir . '/Collections/ReadableCollection.stub'; $files[] = $stubsDir . '/Collections/Collection.stub'; } diff --git a/src/Type/Doctrine/ArgumentsProcessor.php b/src/Type/Doctrine/ArgumentsProcessor.php index f2f88b82..2cd94fd7 100644 --- a/src/Type/Doctrine/ArgumentsProcessor.php +++ b/src/Type/Doctrine/ArgumentsProcessor.php @@ -13,8 +13,7 @@ class ArgumentsProcessor { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; public function __construct(ObjectMetadataResolver $objectMetadataResolver) { @@ -34,6 +33,9 @@ public function processArgs( { $args = []; foreach ($methodCallArgs as $arg) { + if ($arg->unpack) { + throw new DynamicQueryBuilderArgumentException(); + } $value = $scope->getType($arg->value); if ( $value instanceof ExprType @@ -56,7 +58,7 @@ public function processArgs( continue; } - if ($value->isClassStringType()->yes() && count($value->getClassStringObjectType()->getObjectClassNames()) === 1) { + if ($value->isClassString()->yes() && count($value->getClassStringObjectType()->getObjectClassNames()) === 1) { /** @var class-string $className */ $className = $value->getClassStringObjectType()->getObjectClassNames()[0]; if ($this->objectMetadataResolver->isTransient($className)) { diff --git a/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php b/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php index 0d440940..9a177304 100644 --- a/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php +++ b/src/Type/Doctrine/Collection/IsEmptyTypeSpecifyingExtension.php @@ -19,11 +19,10 @@ final class IsEmptyTypeSpecifyingExtension implements MethodTypeSpecifyingExtens private const FIRST_METHOD_NAME = 'first'; private const LAST_METHOD_NAME = 'last'; - /** @var TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; /** @var class-string */ - private $collectionClass; + private string $collectionClass; /** * @param class-string $collectionClass @@ -44,11 +43,8 @@ public function isMethodSupported( TypeSpecifierContext $context ): bool { - return ( - $methodReflection->getDeclaringClass()->getName() === $this->collectionClass - || $methodReflection->getDeclaringClass()->isSubclassOf($this->collectionClass) - ) - && $methodReflection->getName() === self::IS_EMPTY_METHOD_NAME; + return $methodReflection->getDeclaringClass()->is($this->collectionClass) + && $methodReflection->getName() === self::IS_EMPTY_METHOD_NAME; } public function specifyTypes( @@ -61,13 +57,15 @@ public function specifyTypes( $first = $this->typeSpecifier->create( new MethodCall($node->var, self::FIRST_METHOD_NAME), new ConstantBooleanType(false), - $context + $context, + $scope, ); $last = $this->typeSpecifier->create( new MethodCall($node->var, self::LAST_METHOD_NAME), new ConstantBooleanType(false), - $context + $context, + $scope, ); return $first->unionWith($last); diff --git a/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php b/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php index 1cf5d50a..b78f8467 100644 --- a/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php @@ -33,17 +33,13 @@ final class CreateQueryDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var DescriptorRegistry */ - private $descriptorRegistry; + private DescriptorRegistry $descriptorRegistry; - /** @var PhpVersion */ - private $phpVersion; + private PhpVersion $phpVersion; - /** @var DriverDetector */ - private $driverDetector; + private DriverDetector $driverDetector; public function __construct( ObjectMetadataResolver $objectMetadataResolver, @@ -80,7 +76,7 @@ public function getTypeFromMethodCall( if (!isset($args[$queryStringArgIndex])) { return new GenericObjectType( Query::class, - [new MixedType(), new MixedType()] + [new MixedType(), new MixedType()], ); } @@ -113,7 +109,7 @@ public function getTypeFromMethodCall( } return new GenericObjectType( Query::class, - [new MixedType(), new MixedType()] + [new MixedType(), new MixedType()], ); }); } diff --git a/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php b/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php index 31170cfc..0a644f3a 100644 --- a/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php +++ b/src/Type/Doctrine/DBAL/QueryBuilder/QueryBuilderExecuteMethodExtension.php @@ -19,8 +19,7 @@ class QueryBuilderExecuteMethodExtension implements DynamicMethodReturnTypeExtension { - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; public function __construct(ReflectionProvider $reflectionProvider) { @@ -39,7 +38,11 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); $queryBuilderType = new ObjectType(QueryBuilder::class); $var = $methodCall->var; diff --git a/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php new file mode 100644 index 00000000..ac6f38b4 --- /dev/null +++ b/src/Type/Doctrine/DBAL/RowCountMethodDynamicReturnTypeExtension.php @@ -0,0 +1,112 @@ +class = $class; + $this->objectMetadataResolver = $objectMetadataResolver; + $this->driverDetector = $driverDetector; + $this->reflectionProvider = $reflectionProvider; + } + + public function getClass(): string + { + return $this->class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'rowCount'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + $objectManager = $this->objectMetadataResolver->getObjectManager(); + if (!$objectManager instanceof EntityManagerInterface) { + return null; + } + + $connection = $objectManager->getConnection(); + $driver = $this->driverDetector->detect($connection); + if ($driver === null) { + return null; + } + + $resultClass = $this->getResultClass($driver); + + if (!$this->reflectionProvider->hasClass($resultClass)) { + return null; + } + + $resultReflection = $this->reflectionProvider->getClass($resultClass); + if (!$resultReflection->hasNativeMethod('rowCount')) { + return null; + } + + $rowCountMethod = $resultReflection->getNativeMethod('rowCount'); + $variant = $rowCountMethod->getOnlyVariant(); + + return $variant->getReturnType(); + } + + /** + * @param DriverDetector::* $driver + * @return class-string + */ + private function getResultClass(string $driver): string + { + switch ($driver) { + case DriverDetector::IBM_DB2: + return 'Doctrine\DBAL\Driver\IBMDB2\Result'; + case DriverDetector::MYSQLI: + return 'Doctrine\DBAL\Driver\Mysqli\Result'; + case DriverDetector::OCI8: + return 'Doctrine\DBAL\Driver\OCI8\Result'; + case DriverDetector::PDO_MYSQL: + case DriverDetector::PDO_OCI: + case DriverDetector::PDO_PGSQL: + case DriverDetector::PDO_SQLITE: + case DriverDetector::PDO_SQLSRV: + return 'Doctrine\DBAL\Driver\PDO\Result'; + case DriverDetector::PGSQL: + return 'Doctrine\DBAL\Driver\PgSQL\Result'; // @phpstan-ignore return.type + case DriverDetector::SQLITE3: + return 'Doctrine\DBAL\Driver\SQLite3\Result'; // @phpstan-ignore return.type + case DriverDetector::SQLSRV: + return 'Doctrine\DBAL\Driver\SQLSrv\Result'; + } + } + +} diff --git a/src/Type/Doctrine/DefaultDescriptorRegistry.php b/src/Type/Doctrine/DefaultDescriptorRegistry.php index 83647e3b..74ea184d 100644 --- a/src/Type/Doctrine/DefaultDescriptorRegistry.php +++ b/src/Type/Doctrine/DefaultDescriptorRegistry.php @@ -9,7 +9,7 @@ class DefaultDescriptorRegistry implements DescriptorRegistry { /** @var array, DoctrineTypeDescriptor> */ - private $descriptors = []; + private array $descriptors = []; /** * @param DoctrineTypeDescriptor[] $descriptors diff --git a/src/Type/Doctrine/DescriptorRegistryFactory.php b/src/Type/Doctrine/DescriptorRegistryFactory.php index 3e7dd190..2dbf70aa 100644 --- a/src/Type/Doctrine/DescriptorRegistryFactory.php +++ b/src/Type/Doctrine/DescriptorRegistryFactory.php @@ -9,8 +9,7 @@ class DescriptorRegistryFactory public const TYPE_DESCRIPTOR_TAG = 'phpstan.doctrine.typeDescriptor'; - /** @var Container */ - private $container; + private Container $container; public function __construct(Container $container) { diff --git a/src/Type/Doctrine/Descriptors/BigIntType.php b/src/Type/Doctrine/Descriptors/BigIntType.php index 14b3ca2a..01b85eff 100644 --- a/src/Type/Doctrine/Descriptors/BigIntType.php +++ b/src/Type/Doctrine/Descriptors/BigIntType.php @@ -3,7 +3,6 @@ namespace PHPStan\Type\Doctrine\Descriptors; use Composer\InstalledVersions; -use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -25,7 +24,7 @@ public function getWritableToPropertyType(): Type return new IntegerType(); } - return TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()); + return (new IntegerType())->toString(); } public function getWritableToDatabaseType(): Type diff --git a/src/Type/Doctrine/Descriptors/BooleanType.php b/src/Type/Doctrine/Descriptors/BooleanType.php index b9e59574..54e7f662 100644 --- a/src/Type/Doctrine/Descriptors/BooleanType.php +++ b/src/Type/Doctrine/Descriptors/BooleanType.php @@ -12,8 +12,7 @@ class BooleanType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor { - /** @var DriverDetector */ - private $driverDetector; + private DriverDetector $driverDetector; public function __construct(DriverDetector $driverDetector) { @@ -40,7 +39,7 @@ public function getDatabaseInternalType(): Type return TypeCombinator::union( new ConstantIntegerType(0), new ConstantIntegerType(1), - new \PHPStan\Type\BooleanType() + new \PHPStan\Type\BooleanType(), ); } @@ -60,7 +59,7 @@ public function getDatabaseInternalTypeForDriver(Connection $connection): Type ], true)) { return TypeCombinator::union( new ConstantIntegerType(0), - new ConstantIntegerType(1) + new ConstantIntegerType(1), ); } diff --git a/src/Type/Doctrine/Descriptors/DecimalType.php b/src/Type/Doctrine/Descriptors/DecimalType.php index 64184c45..0f0e16e3 100644 --- a/src/Type/Doctrine/Descriptors/DecimalType.php +++ b/src/Type/Doctrine/Descriptors/DecimalType.php @@ -4,10 +4,8 @@ use Doctrine\DBAL\Connection; use PHPStan\Doctrine\Driver\DriverDetector; -use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; -use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -16,8 +14,7 @@ class DecimalType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor { - /** @var DriverDetector */ - private $driverDetector; + private DriverDetector $driverDetector; public function __construct(DriverDetector $driverDetector) { @@ -31,7 +28,7 @@ public function getType(): string public function getWritableToPropertyType(): Type { - return TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()); + return (new FloatType())->toString(); } public function getWritableToDatabaseType(): Type @@ -58,10 +55,7 @@ public function getDatabaseInternalTypeForDriver(Connection $connection): Type DriverDetector::PGSQL, DriverDetector::PDO_PGSQL, ], true)) { - return new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); + return (new FloatType())->toString(); } // not yet supported driver, return the old implementation guess diff --git a/src/Type/Doctrine/Descriptors/FloatType.php b/src/Type/Doctrine/Descriptors/FloatType.php index 2518e72d..7f472ceb 100644 --- a/src/Type/Doctrine/Descriptors/FloatType.php +++ b/src/Type/Doctrine/Descriptors/FloatType.php @@ -4,10 +4,7 @@ use Doctrine\DBAL\Connection; use PHPStan\Doctrine\Driver\DriverDetector; -use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\IntegerType; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function in_array; @@ -15,8 +12,7 @@ class FloatType implements DoctrineTypeDescriptor, DoctrineTypeDriverAwareDescriptor { - /** @var DriverDetector */ - private $driverDetector; + private DriverDetector $driverDetector; public function __construct(DriverDetector $driverDetector) { @@ -42,10 +38,7 @@ public function getDatabaseInternalType(): Type { return TypeCombinator::union( new \PHPStan\Type\FloatType(), - new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]) + (new \PHPStan\Type\FloatType())->toString(), ); } @@ -53,18 +46,12 @@ public function getDatabaseInternalTypeForDriver(Connection $connection): Type { $driverType = $this->driverDetector->detect($connection); - if ($driverType === DriverDetector::PDO_PGSQL) { - return new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); - } - if (in_array($driverType, [ DriverDetector::SQLITE3, DriverDetector::PDO_SQLITE, DriverDetector::MYSQLI, DriverDetector::PDO_MYSQL, + DriverDetector::PDO_PGSQL, DriverDetector::PGSQL, ], true)) { return new \PHPStan\Type\FloatType(); diff --git a/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php b/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php index 78501f2c..549b14c4 100644 --- a/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php +++ b/src/Type/Doctrine/Descriptors/Ramsey/UuidTypeDescriptor.php @@ -23,8 +23,7 @@ class UuidTypeDescriptor implements DoctrineTypeDescriptor FakeTestingUuidType::class, ]; - /** @var string */ - private $uuidTypeName; + private string $uuidTypeName; public function __construct( string $uuidTypeName @@ -33,7 +32,7 @@ public function __construct( if (!in_array($uuidTypeName, self::SUPPORTED_UUID_TYPES, true)) { throw new ShouldNotHappenException(sprintf( 'Unexpected UUID column type "%s" provided', - $uuidTypeName + $uuidTypeName, )); } @@ -55,7 +54,7 @@ public function getWritableToDatabaseType(): Type { return TypeCombinator::union( new StringType(), - new ObjectType(UuidInterface::class) + new ObjectType(UuidInterface::class), ); } diff --git a/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php b/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php index 7d7cb778..f4b5dba0 100644 --- a/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php +++ b/src/Type/Doctrine/Descriptors/ReflectionDescriptor.php @@ -21,11 +21,9 @@ class ReflectionDescriptor implements DoctrineTypeDescriptor, DoctrineTypeDriver /** @var class-string */ private $type; - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; - /** @var Container */ - private $container; + private Container $container; /** * @param class-string $type diff --git a/src/Type/Doctrine/Descriptors/SimpleArrayType.php b/src/Type/Doctrine/Descriptors/SimpleArrayType.php index 8044caca..2154eb91 100644 --- a/src/Type/Doctrine/Descriptors/SimpleArrayType.php +++ b/src/Type/Doctrine/Descriptors/SimpleArrayType.php @@ -8,6 +8,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; class SimpleArrayType implements DoctrineTypeDescriptor { @@ -19,7 +20,7 @@ public function getType(): string public function getWritableToPropertyType(): Type { - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), new StringType()), new AccessoryArrayListType()); } public function getWritableToDatabaseType(): Type diff --git a/src/Type/Doctrine/EntityManagerInterfaceThrowTypeExtension.php b/src/Type/Doctrine/EntityManagerInterfaceThrowTypeExtension.php index 0070af9f..be3407c3 100644 --- a/src/Type/Doctrine/EntityManagerInterfaceThrowTypeExtension.php +++ b/src/Type/Doctrine/EntityManagerInterfaceThrowTypeExtension.php @@ -37,9 +37,7 @@ public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, M 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()]) + ...array_map(static fn ($class): Type => new ObjectType($class), self::SUPPORTED_METHOD[$methodReflection->getName()]), ); } diff --git a/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php b/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php index fd90fdd5..07d40ffa 100644 --- a/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/GetRepositoryDynamicReturnTypeExtension.php @@ -27,24 +27,22 @@ class GetRepositoryDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; - /** @var string|null */ - private $repositoryClass; + private ?string $repositoryClass = null; - /** @var string|null */ - private $ormRepositoryClass; + private ?string $ormRepositoryClass = null; - /** @var string|null */ - private $odmRepositoryClass; + private ?string $odmRepositoryClass = null; - /** @var string */ - private $managerClass; + /** @var class-string */ + private string $managerClass; - /** @var ObjectMetadataResolver */ - private $metadataResolver; + private ObjectMetadataResolver $metadataResolver; + /** + * @param class-string $managerClass + */ public function __construct( ReflectionProvider $reflectionProvider, ?string $repositoryClass, @@ -87,11 +85,11 @@ public function getTypeFromMethodCall( if (count($methodCall->getArgs()) === 0) { return new GenericObjectType( $defaultRepositoryClass, - [new ObjectWithoutClassType()] + [new ObjectWithoutClassType()], ); } $argType = $scope->getType($methodCall->getArgs()[0]->value); - if (!$argType->isClassStringType()->yes()) { + if (!$argType->isClassString()->yes()) { return $this->getDefaultReturnType($scope, $methodCall->getArgs(), $methodReflection, $defaultRepositoryClass); } @@ -101,7 +99,7 @@ public function getTypeFromMethodCall( if (count($objectNames) === 0) { return new GenericObjectType( $defaultRepositoryClass, - [$classType] + [$classType], ); } @@ -127,13 +125,13 @@ private function getDefaultReturnType(Scope $scope, array $args, MethodReflectio $defaultType = ParametersAcceptorSelector::selectFromArgs( $scope, $args, - $methodReflection->getVariants() + $methodReflection->getVariants(), )->getReturnType(); $entity = $defaultType->getTemplateType(ObjectRepository::class, 'TEntityClass'); if (!$entity instanceof ErrorType) { return new GenericObjectType( $defaultRepositoryClass, - [$entity] + [$entity], ); } diff --git a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php index 2f1c8e36..e978ae2f 100644 --- a/src/Type/Doctrine/HydrationModeReturnTypeResolver.php +++ b/src/Type/Doctrine/HydrationModeReturnTypeResolver.php @@ -73,18 +73,18 @@ public function getMethodReturnTypeForHydrationMode( case 'toIterable': return new IterableType( $queryKeyType->isNull()->yes() ? new IntegerType() : $queryKeyType, - $queryResultType + $queryResultType, ); default: if ($queryKeyType->isNull()->yes()) { - return AccessoryArrayListType::intersectWith(new ArrayType( + return TypeCombinator::intersect(new ArrayType( new IntegerType(), - $queryResultType - )); + $queryResultType, + ), new AccessoryArrayListType()); } return new ArrayType( $queryKeyType, - $queryResultType + $queryResultType, ); } } diff --git a/src/Type/Doctrine/ObjectMetadataResolver.php b/src/Type/Doctrine/ObjectMetadataResolver.php index 6a8b4fd2..054e9a57 100644 --- a/src/Type/Doctrine/ObjectMetadataResolver.php +++ b/src/Type/Doctrine/ObjectMetadataResolver.php @@ -17,17 +17,14 @@ final class ObjectMetadataResolver { - /** @var string|null */ - private $objectManagerLoader; + private ?string $objectManagerLoader = null; /** @var ObjectManager|false|null */ private $objectManager; - /** @var ClassMetadataFactory|null */ - private $metadataFactory; + private ?ClassMetadataFactory $metadataFactory = null; - /** @var string */ - private $tmpDir; + private string $tmpDir; public function __construct( ?string $objectManagerLoader, @@ -106,6 +103,8 @@ private function getMetadataFactory(): ?ClassMetadataFactory } /** + * @api + * * @template T of object * @param class-string $className * @return ClassMetadata|null @@ -150,14 +149,14 @@ private function loadObjectManager(string $objectManagerLoader): ?ObjectManager if (!is_file($objectManagerLoader)) { throw new ShouldNotHappenException(sprintf( 'Object manager could not be loaded: file "%s" does not exist', - $objectManagerLoader + $objectManagerLoader, )); } if (!is_readable($objectManagerLoader)) { throw new ShouldNotHappenException(sprintf( 'Object manager could not be loaded: file "%s" is not readable', - $objectManagerLoader + $objectManagerLoader, )); } diff --git a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php index 091e3716..22ed0b5a 100644 --- a/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/Query/QueryResultDynamicReturnTypeExtension.php @@ -27,11 +27,9 @@ final class QueryResultDynamicReturnTypeExtension implements DynamicMethodReturn 'getSingleResult' => 0, ]; - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var HydrationModeReturnTypeResolver */ - private $hydrationModeReturnTypeResolver; + private HydrationModeReturnTypeResolver $hydrationModeReturnTypeResolver; public function __construct( ObjectMetadataResolver $objectMetadataResolver, @@ -70,8 +68,10 @@ public function getTypeFromMethodCall( if (isset($args[$argIndex])) { $hydrationMode = $scope->getType($args[$argIndex]->value); } else { - $parametersAcceptor = ParametersAcceptorSelector::selectSingle( - $methodReflection->getVariants() + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), ); $parameter = $parametersAcceptor->getParameters()[$argIndex]; $hydrationMode = $parameter->getDefaultValue() ?? new NullType(); @@ -84,7 +84,7 @@ public function getTypeFromMethodCall( $hydrationMode, $queryType->getTemplateType(AbstractQuery::class, 'TKey'), $queryType->getTemplateType(AbstractQuery::class, 'TResult'), - $this->objectMetadataResolver->getObjectManager() + $this->objectMetadataResolver->getObjectManager(), ); } diff --git a/src/Type/Doctrine/Query/QueryResultTypeBuilder.php b/src/Type/Doctrine/Query/QueryResultTypeBuilder.php index 30941cb3..2f6fae11 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeBuilder.php +++ b/src/Type/Doctrine/Query/QueryResultTypeBuilder.php @@ -21,15 +21,13 @@ final class QueryResultTypeBuilder { - /** @var bool */ - private $selectQuery = false; + private bool $selectQuery = false; /** * Whether the result is an array shape or a single entity or NEW object * - * @var bool */ - private $isShape = false; + private bool $isShape = false; /** * Map from selected entity aliases to entity types @@ -38,7 +36,7 @@ final class QueryResultTypeBuilder * * @var array */ - private $entities = []; + private array $entities = []; /** * Map from selected entity alias to result alias @@ -47,24 +45,23 @@ final class QueryResultTypeBuilder * * @var array */ - private $entityResultAliases = []; + private array $entityResultAliases = []; /** * Map from selected scalar result alias to scalar type * * @var array */ - private $scalars = []; + private array $scalars = []; /** * Map from selected NEW objcet result alias to NEW object type * * @var array */ - private $newObjects = []; + private array $newObjects = []; - /** @var Type */ - private $indexedBy; + private Type $indexedBy; public function __construct() { diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 2ce3e1ce..128ae674 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -18,7 +18,10 @@ use PHPStan\Php\PhpVersion; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; +use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; @@ -39,6 +42,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use function array_key_exists; use function array_map; @@ -82,53 +86,45 @@ class QueryResultTypeWalker extends SqlWalker /** * Counter for generating unique scalar result. * - * @var int */ - private $scalarResultCounter = 1; + private int $scalarResultCounter = 1; /** * Counter for generating indexes. * - * @var int */ - private $newObjectCounter = 0; + private int $newObjectCounter = 0; /** @var Query */ - private $query; + private Query $query; - /** @var EntityManagerInterface */ - private $em; + private EntityManagerInterface $em; - /** @var PhpVersion */ - private $phpVersion; + private PhpVersion $phpVersion; /** @var DriverDetector::*|null */ private $driverType; /** @var array */ - private $driverOptions; + private array $driverOptions; /** * Map of all components/classes that appear in the DQL query. * * @var array $queryComponents */ - private $queryComponents; + private array $queryComponents; /** @var array */ - private $nullableQueryComponents; + private array $nullableQueryComponents; - /** @var QueryResultTypeBuilder */ - private $typeBuilder; + private QueryResultTypeBuilder $typeBuilder; - /** @var DescriptorRegistry */ - private $descriptorRegistry; + private DescriptorRegistry $descriptorRegistry; - /** @var bool */ - private $hasAggregateFunction; + private bool $hasAggregateFunction; - /** @var bool */ - private $hasGroupByClause; + private bool $hasGroupByClause; /** @@ -180,7 +176,7 @@ public function __construct($query, $parserResult, array $queryComponents) 'Expected the query hint %s to contain a %s, but got a %s', self::HINT_TYPE_MAPPING, QueryResultTypeBuilder::class, - is_object($typeBuilder) ? get_class($typeBuilder) : gettype($typeBuilder) + is_object($typeBuilder) ? get_class($typeBuilder) : gettype($typeBuilder), )); } @@ -193,7 +189,7 @@ public function __construct($query, $parserResult, array $queryComponents) 'Expected the query hint %s to contain a %s, but got a %s', self::HINT_DESCRIPTOR_REGISTRY, DescriptorRegistry::class, - is_object($descriptorRegistry) ? get_class($descriptorRegistry) : gettype($descriptorRegistry) + is_object($descriptorRegistry) ? get_class($descriptorRegistry) : gettype($descriptorRegistry), )); } @@ -206,7 +202,7 @@ public function __construct($query, $parserResult, array $queryComponents) 'Expected the query hint %s to contain a %s, but got a %s', self::HINT_PHP_VERSION, PhpVersion::class, - is_object($phpVersion) ? get_class($phpVersion) : gettype($phpVersion) + is_object($phpVersion) ? get_class($phpVersion) : gettype($phpVersion), )); } @@ -219,7 +215,7 @@ public function __construct($query, $parserResult, array $queryComponents) 'Expected the query hint %s to contain a %s, but got a %s', self::HINT_DRIVER_DETECTOR, DriverDetector::class, - is_object($driverDetector) ? get_class($driverDetector) : gettype($driverDetector) + is_object($driverDetector) ? get_class($driverDetector) : gettype($driverDetector), )); } $connection = $this->em->getConnection(); @@ -469,10 +465,6 @@ public function walkFunction($function): string } if ($this->containsOnlyNumericTypes($exprTypeNoNull)) { - if ($this->driverType === DriverDetector::PDO_PGSQL) { - return $this->marshalType($this->createNumericString($nullable)); - } - return $this->marshalType($exprType); // retains underlying type } @@ -619,29 +611,23 @@ public function walkFunction($function): string if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { $type = new FloatType(); - $cannotBeNegative = $exprType->isSmallerThan(new ConstantIntegerType(0))->no(); + $cannotBeNegative = $exprType->isSmallerThan(new ConstantIntegerType(0), $this->phpVersion)->no(); $canBeNegative = !$cannotBeNegative; if ($canBeNegative) { $type = TypeCombinator::addNull($type); } - } elseif ($this->driverType === DriverDetector::PDO_PGSQL) { - $type = new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); - - } elseif ($this->driverType === DriverDetector::PGSQL) { + } elseif ($this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::PDO_PGSQL) { $castedExprType = $this->castStringLiteralForNumericExpression($exprTypeNoNull); if ($castedExprType->isInteger()->yes() || $castedExprType->isFloat()->yes()) { $type = $this->createFloat(false); } elseif ($castedExprType->isNumericString()->yes()) { - $type = $this->createNumericString(false); + $type = $this->createNumericString(false, $castedExprType->isLowercaseString()->yes(), $castedExprType->isUppercaseString()->yes()); } else { - $type = TypeCombinator::union($this->createFloat(false), $this->createNumericString(false)); + $type = TypeCombinator::union($this->createFloat(false), $this->createNumericString(false, false, true)); } } else { @@ -754,7 +740,7 @@ private function inferAvgFunction(AST\Functions\AvgFunction $function): Type if ($this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::MYSQLI) { if ($exprTypeNoNull->isInteger()->yes()) { - return $this->createNumericString($nullable); + return $this->createNumericString($nullable, true, true); } if ($exprTypeNoNull->isString()->yes() && !$exprTypeNoNull->isNumericString()->yes()) { @@ -766,7 +752,7 @@ private function inferAvgFunction(AST\Functions\AvgFunction $function): Type if ($this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::PDO_PGSQL) { if ($exprTypeNoNull->isInteger()->yes()) { - return $this->createNumericString($nullable); + return $this->createNumericString($nullable, true, true); } return $this->generalizeConstantType($exprType, $nullable); @@ -802,7 +788,7 @@ private function inferSumFunction(AST\Functions\SumFunction $function): Type if ($this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::MYSQLI) { if ($exprTypeNoNull->isInteger()->yes()) { - return $this->createNumericString($nullable); + return $this->createNumericString($nullable, true, true); } if ($exprTypeNoNull->isString()->yes() && !$exprTypeNoNull->isNumericString()->yes()) { @@ -816,7 +802,7 @@ private function inferSumFunction(AST\Functions\SumFunction $function): Type if ($exprTypeNoNull->isInteger()->yes()) { return TypeCombinator::union( $this->createInteger($nullable), - $this->createNumericString($nullable) + $this->createNumericString($nullable, true, true), ); } @@ -836,7 +822,7 @@ private function createFloatOrInt(bool $nullable): Type { $union = TypeCombinator::union( new FloatType(), - new IntegerType() + new IntegerType(), ); return $nullable ? TypeCombinator::addNull($union) : $union; } @@ -853,19 +839,41 @@ private function createNonNegativeInteger(bool $nullable): Type return $nullable ? TypeCombinator::addNull($integer) : $integer; } - private function createNumericString(bool $nullable): Type + private function createNumericString(bool $nullable, bool $lowercase = false, bool $uppercase = false): Type { - $numericString = TypeCombinator::intersect( + $types = [ new StringType(), - new AccessoryNumericStringType() - ); + new AccessoryNumericStringType(), + ]; + if ($lowercase) { + $types[] = new AccessoryLowercaseStringType(); + } + if ($uppercase) { + $types[] = new AccessoryUppercaseStringType(); + } + + $numericString = new IntersectionType($types); return $nullable ? TypeCombinator::addNull($numericString) : $numericString; } - private function createString(bool $nullable): Type + private function createString(bool $nullable, bool $lowercase = false, bool $uppercase = false): Type { - $string = new StringType(); + if ($lowercase || $uppercase) { + $types = [ + new StringType(), + ]; + if ($lowercase) { + $types[] = new AccessoryLowercaseStringType(); + } + if ($uppercase) { + $types[] = new AccessoryUppercaseStringType(); + } + $string = new IntersectionType($types); + } else { + $string = new StringType(); + } + return $nullable ? TypeCombinator::addNull($string) : $string; } @@ -911,10 +919,18 @@ private function generalizeConstantType(Type $type, bool $makeNullable): Type $result = $this->createFloat($containsNull); } elseif ($typeNoNull->isNumericString()->yes()) { - $result = $this->createNumericString($containsNull); + $result = $this->createNumericString( + $containsNull, + $typeNoNull->isLowercaseString()->yes(), + $typeNoNull->isUppercaseString()->yes(), + ); } elseif ($typeNoNull->isString()->yes()) { - $result = $this->createString($containsNull); + $result = $this->createString( + $containsNull, + $typeNoNull->isLowercaseString()->yes(), + $typeNoNull->isUppercaseString()->yes(), + ); } else { $result = $type; @@ -1007,7 +1023,7 @@ public function walkCoalesceExpression($coalesceExpression): string if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL) { return $this->marshalType( - $this->inferCoalesceForMySql($rawTypes, $generalizedUnion) + $this->inferCoalesceForMySql($rawTypes, $generalizedUnion), ); } @@ -1089,13 +1105,13 @@ public function walkGeneralCaseExpression(AST\GeneralCaseExpression $generalCase } $types[] = $this->unmarshalType( - $thenScalarExpression->dispatch($this) + $thenScalarExpression->dispatch($this), ); } if ($elseScalarExpression instanceof AST\Node) { $types[] = $this->unmarshalType( - $elseScalarExpression->dispatch($this) + $elseScalarExpression->dispatch($this), ); } @@ -1126,13 +1142,13 @@ public function walkSimpleCaseExpression($simpleCaseExpression): string } $types[] = $this->unmarshalType( - $thenScalarExpression->dispatch($this) + $thenScalarExpression->dispatch($this), ); } if ($elseScalarExpression instanceof AST\Node) { $types[] = $this->unmarshalType( - $elseScalarExpression->dispatch($this) + $elseScalarExpression->dispatch($this), ); } @@ -1223,7 +1239,7 @@ public function walkSelectExpression($selectExpression): string $dbalTypeName = DbalType::getTypeRegistry()->lookupName($expr->getReturnType()); $type = TypeCombinator::intersect( // e.g. count is typed as int, but we infer int<0, max> $type, - $this->resolveDoctrineType($dbalTypeName, null, TypeCombinator::containsNull($type)) + $this->resolveDoctrineType($dbalTypeName, null, TypeCombinator::containsNull($type)), ); if ($this->hasAggregateWithoutGroupBy() && !$expr instanceof AST\Functions\CountFunction) { @@ -1257,7 +1273,7 @@ public function walkSelectExpression($selectExpression): string // e.g. 1.0 on sqlite results to '1' with pdo_stringify on PHP 8.1, but '1.0' on PHP 8.0 with no setup // so we relax constant types and return just numeric-string to avoid those issues - $stringifiedFloat = $this->createNumericString(false); + $stringifiedFloat = $this->createNumericString(false, false, true); if ($stringify->yes()) { return $stringifiedFloat; @@ -1687,7 +1703,7 @@ public function walkSimpleArithmeticExpression($simpleArithmeticExpr): string } $types[] = $this->castStringLiteralForNumericExpression( - $this->unmarshalType($this->walkArithmeticPrimary($term)) + $this->unmarshalType($this->walkArithmeticPrimary($term)), ); } @@ -1714,7 +1730,7 @@ public function walkArithmeticTerm($term): string } $types[] = $this->castStringLiteralForNumericExpression( - $this->unmarshalType($this->walkArithmeticPrimary($factor)) + $this->unmarshalType($this->walkArithmeticPrimary($factor)), ); } @@ -1769,12 +1785,6 @@ private function inferPlusMinusTimesType(array $termTypes): Type return $this->createInteger($nullable); } - if ($this->driverType === DriverDetector::PDO_PGSQL) { - if ($this->containsOnlyNumericTypes($unionWithoutNull)) { - return $this->createNumericString($nullable); - } - } - if ($this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { if (!$this->containsOnlyNumericTypes(...$typesNoNull)) { return new MixedType(); @@ -1789,13 +1799,17 @@ private function inferPlusMinusTimesType(array $termTypes): Type return $this->createFloatOrInt($nullable); } - if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::PGSQL) { + if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::PDO_PGSQL) { if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), new FloatType()])) { return $this->createFloat($nullable); } if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), $this->createNumericString(false)])) { - return $this->createNumericString($nullable); + return $this->createNumericString( + $nullable, + $unionWithoutNull->toString()->isLowercaseString()->yes(), + $unionWithoutNull->toString()->isUppercaseString()->yes(), + ); } if ($this->containsOnlyNumericTypes($unionWithoutNull)) { @@ -1847,7 +1861,7 @@ private function inferDivisionType(array $termTypes): Type if ($unionWithoutNull->isInteger()->yes()) { if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL) { - return $this->createNumericString($nullable); + return $this->createNumericString($nullable, true, true); } elseif ($this->driverType === DriverDetector::PDO_PGSQL || $this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { return $this->createInteger($nullable); } @@ -1855,12 +1869,6 @@ private function inferDivisionType(array $termTypes): Type return new MixedType(); } - if ($this->driverType === DriverDetector::PDO_PGSQL) { - if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), new FloatType(), $this->createNumericString(false)])) { - return $this->createNumericString($nullable); - } - } - if ($this->driverType === DriverDetector::SQLITE3 || $this->driverType === DriverDetector::PDO_SQLITE) { if (!$this->containsOnlyNumericTypes(...$typesNoNull)) { return new MixedType(); @@ -1875,13 +1883,17 @@ private function inferDivisionType(array $termTypes): Type return $this->createFloatOrInt($nullable); } - if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::PGSQL) { + if ($this->driverType === DriverDetector::MYSQLI || $this->driverType === DriverDetector::PDO_MYSQL || $this->driverType === DriverDetector::PGSQL || $this->driverType === DriverDetector::PDO_PGSQL) { if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), new FloatType()])) { return $this->createFloat($nullable); } if ($this->containsOnlyTypes($unionWithoutNull, [new IntegerType(), $this->createNumericString(false)])) { - return $this->createNumericString($nullable); + return $this->createNumericString( + $nullable, + $unionWithoutNull->toString()->isLowercaseString()->yes(), + $unionWithoutNull->toString()->isUppercaseString()->yes(), + ); } if ($this->containsOnlyTypes($unionWithoutNull, [new FloatType(), $this->createNumericString(false)])) { @@ -1915,7 +1927,7 @@ public function walkArithmeticFactor($factor): string } elseif ($type instanceof IntegerRangeType && $factor->sign === false) { $type = IntegerRangeType::fromInterval( $type->getMax() === null ? null : $type->getMax() * -1, - $type->getMin() === null ? null : $type->getMin() * -1 + $type->getMin() === null ? null : $type->getMin() * -1, ); } elseif ($type instanceof ConstantFloatType && $factor->sign === false) { @@ -2009,17 +2021,28 @@ private function getTypeOfField(ClassMetadata $class, string $fieldName): array /** @param ?class-string $enumType */ private function resolveDoctrineType(string $typeName, ?string $enumType = null, bool $nullable = false): Type { - if ($enumType !== null) { - $type = new ObjectType($enumType); - } else { - try { - $type = $this->descriptorRegistry - ->get($typeName) - ->getWritableToPropertyType(); - if ($type instanceof NeverType) { - $type = new MixedType(); + try { + $type = $this->descriptorRegistry + ->get($typeName) + ->getWritableToPropertyType(); + + if ($enumType !== null) { + if ($type->isArray()->no()) { + $type = new ObjectType($enumType); + } else { + $type = TypeCombinator::intersect(new ArrayType( + $type->getIterableKeyType(), + new ObjectType($enumType), + ), ...TypeUtils::getAccessoryTypes($type)); } - } catch (DescriptorNotRegisteredException $e) { + } + if ($type instanceof NeverType) { + $type = new MixedType(); + } + } catch (DescriptorNotRegisteredException $e) { + if ($enumType !== null) { + $type = new ObjectType($enumType); + } else { $type = new MixedType(); } } @@ -2028,7 +2051,7 @@ private function resolveDoctrineType(string $typeName, ?string $enumType = null, $type = TypeCombinator::addNull($type); } - return $type; + return $type; } /** @param ?class-string $enumType */ @@ -2045,9 +2068,7 @@ private function resolveDatabaseInternalType(string $typeName, ?string $enumType } if ($enumType !== null) { - $enumTypes = array_map(static function ($enumType) { - return ConstantTypeHelper::getTypeFromValue($enumType->value); - }, $enumType::cases()); + $enumTypes = array_map(static fn ($enumType) => ConstantTypeHelper::getTypeFromValue($enumType->value), $enumType::cases()); $enumType = TypeCombinator::union(...$enumTypes); $enumType = TypeCombinator::union($enumType, $enumType->toString()); $type = TypeCombinator::intersect($enumType, $type); @@ -2087,6 +2108,9 @@ private function hasAggregateWithoutGroupBy(): bool * - pdo_sqlite: https://github.com/php/php-src/commit/438b025a28cda2935613af412fc13702883dd3a2 * - pdo_pgsql: https://github.com/php/php-src/commit/737195c3ae6ac53b9501cfc39cc80fd462909c82 * + * Notable 8.4 changes: + * - pdo_pgsql: https://github.com/php/php-src/commit/6d10a6989897e9089d62edf939344437128e93ad + * * @param IntegerType|FloatType|BooleanType $type */ private function shouldStringifyExpressions(Type $type): TrinaryLogic @@ -2131,7 +2155,14 @@ private function shouldStringifyExpressions(Type $type): TrinaryLogic } return TrinaryLogic::createNo(); + } + if ($type->isFloat()->yes()) { + if ($this->phpVersion->getVersionId() >= 80400) { + return TrinaryLogic::createFromBoolean($stringifyFetches); + } + + return TrinaryLogic::createYes(); } return TrinaryLogic::createFromBoolean($stringifyFetches); diff --git a/src/Type/Doctrine/Query/QueryType.php b/src/Type/Doctrine/Query/QueryType.php index b0cb7968..53842dd2 100644 --- a/src/Type/Doctrine/Query/QueryType.php +++ b/src/Type/Doctrine/Query/QueryType.php @@ -2,8 +2,8 @@ namespace PHPStan\Type\Doctrine\Query; -use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -11,14 +11,11 @@ class QueryType extends GenericObjectType { - /** @var Type */ - private $indexType; + private Type $indexType; - /** @var Type */ - private $resultType; + private Type $resultType; - /** @var string */ - private $dql; + private string $dql; public function __construct(string $dql, ?Type $indexType = null, ?Type $resultType = null, ?Type $subtractedType = null) { @@ -47,10 +44,10 @@ public function changeSubtractedType(?Type $subtractedType): Type return new self('Doctrine\ORM\Query', $this->indexType, $this->resultType, $subtractedType); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof self) { - return TrinaryLogic::createFromBoolean($this->equals($type)); + return IsSuperTypeOfResult::createFromBoolean($this->equals($type)); } return parent::isSuperTypeOf($type); diff --git a/src/Type/Doctrine/QueryBuilder/BranchingQueryBuilderType.php b/src/Type/Doctrine/QueryBuilder/BranchingQueryBuilderType.php index e9c7bfb8..473dffb5 100644 --- a/src/Type/Doctrine/QueryBuilder/BranchingQueryBuilderType.php +++ b/src/Type/Doctrine/QueryBuilder/BranchingQueryBuilderType.php @@ -2,7 +2,7 @@ namespace PHPStan\Type\Doctrine\QueryBuilder; -use PHPStan\TrinaryLogic; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\Type; use function array_keys; use function count; @@ -35,10 +35,10 @@ public function equals(Type $type): bool return parent::equals($type); } - public function isSuperTypeOf(Type $type): TrinaryLogic + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult { if ($type instanceof parent) { - return TrinaryLogic::createFromBoolean($this->equals($type)); + return IsSuperTypeOfResult::createFromBoolean($this->equals($type)); } return parent::isSuperTypeOf($type); diff --git a/src/Type/Doctrine/QueryBuilder/CreateQueryBuilderDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/CreateQueryBuilderDynamicReturnTypeExtension.php index 263f2284..3fe7e9b1 100644 --- a/src/Type/Doctrine/QueryBuilder/CreateQueryBuilderDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/CreateQueryBuilderDynamicReturnTypeExtension.php @@ -11,19 +11,13 @@ class CreateQueryBuilderDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var string|null */ - private $queryBuilderClass; - - /** @var bool */ - private $fasterVersion; + private ?string $queryBuilderClass = null; public function __construct( - ?string $queryBuilderClass, - bool $fasterVersion + ?string $queryBuilderClass ) { $this->queryBuilderClass = $queryBuilderClass; - $this->fasterVersion = $fasterVersion; } public function getClass(): string @@ -42,13 +36,8 @@ public function getTypeFromMethodCall( Scope $scope ): Type { - $class = SimpleQueryBuilderType::class; - if (!$this->fasterVersion) { - $class = BranchingQueryBuilderType::class; - } - - return new $class( - $this->queryBuilderClass ?? 'Doctrine\ORM\QueryBuilder' + return new BranchingQueryBuilderType( + $this->queryBuilderClass ?? 'Doctrine\ORM\QueryBuilder', ); } diff --git a/src/Type/Doctrine/QueryBuilder/EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension.php index f170b550..44201e45 100644 --- a/src/Type/Doctrine/QueryBuilder/EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/EntityRepositoryCreateQueryBuilderDynamicReturnTypeExtension.php @@ -35,7 +35,7 @@ public function getTypeFromMethodCall( $entityNameExpr = new MethodCall($methodCall->var, new Identifier('getEntityName')); $entityNameExprType = $scope->getType($entityNameExpr); - if ($entityNameExprType->isClassStringType()->yes() && count($entityNameExprType->getClassStringObjectType()->getObjectClassNames()) === 1) { + if ($entityNameExprType->isClassString()->yes() && count($entityNameExprType->getClassStringObjectType()->getObjectClassNames()) === 1) { $entityNameExpr = new String_($entityNameExprType->getClassStringObjectType()->getObjectClassNames()[0]); } diff --git a/src/Type/Doctrine/QueryBuilder/Expr/BaseExpressionDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/Expr/BaseExpressionDynamicReturnTypeExtension.php index d797b45f..7dd9729e 100644 --- a/src/Type/Doctrine/QueryBuilder/Expr/BaseExpressionDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/Expr/BaseExpressionDynamicReturnTypeExtension.php @@ -17,8 +17,7 @@ class BaseExpressionDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var ArgumentsProcessor */ - private $argumentsProcessor; + private ArgumentsProcessor $argumentsProcessor; public function __construct( ArgumentsProcessor $argumentsProcessor diff --git a/src/Type/Doctrine/QueryBuilder/Expr/ExprType.php b/src/Type/Doctrine/QueryBuilder/Expr/ExprType.php index 00610d03..30a97a59 100644 --- a/src/Type/Doctrine/QueryBuilder/Expr/ExprType.php +++ b/src/Type/Doctrine/QueryBuilder/Expr/ExprType.php @@ -8,8 +8,7 @@ class ExprType extends ObjectType { - /** @var object */ - private $exprObject; + private object $exprObject; /** * @param object $exprObject diff --git a/src/Type/Doctrine/QueryBuilder/Expr/ExpressionBuilderDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/Expr/ExpressionBuilderDynamicReturnTypeExtension.php index fae7cd28..a6a6896a 100644 --- a/src/Type/Doctrine/QueryBuilder/Expr/ExpressionBuilderDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/Expr/ExpressionBuilderDynamicReturnTypeExtension.php @@ -18,11 +18,9 @@ class ExpressionBuilderDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var ArgumentsProcessor */ - private $argumentsProcessor; + private ArgumentsProcessor $argumentsProcessor; public function __construct( ObjectMetadataResolver $objectMetadataResolver, diff --git a/src/Type/Doctrine/QueryBuilder/Expr/NewExprDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/Expr/NewExprDynamicReturnTypeExtension.php index 1690c63c..79206e1c 100644 --- a/src/Type/Doctrine/QueryBuilder/Expr/NewExprDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/Expr/NewExprDynamicReturnTypeExtension.php @@ -18,15 +18,16 @@ class NewExprDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { - /** @var ArgumentsProcessor */ - private $argumentsProcessor; + private ArgumentsProcessor $argumentsProcessor; - /** @var string */ - private $class; + /** @var class-string */ + private string $class; - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; + /** + * @param class-string $class + */ public function __construct( ArgumentsProcessor $argumentsProcessor, string $class, @@ -68,8 +69,8 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, ...$this->argumentsProcessor->processArgs( $scope, $methodReflection->getName(), - $methodCall->getArgs() - ) + $methodCall->getArgs(), + ), ); } catch (DynamicQueryBuilderArgumentException $e) { return new ObjectType($this->reflectionProvider->getClassName($className)); diff --git a/src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php b/src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php index 4375c17a..153291f5 100644 --- a/src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php +++ b/src/Type/Doctrine/QueryBuilder/OtherMethodQueryBuilderParser.php @@ -28,25 +28,19 @@ class OtherMethodQueryBuilderParser { - /** @var bool */ - private $descendIntoOtherMethods; + private Parser $parser; - /** @var Parser */ - private $parser; - - /** @var Container */ - private $container; + private Container $container; /** * Null if the method is currently being processed * * @var array|null> */ - private $cache = []; + private array $cache = []; - public function __construct(bool $descendIntoOtherMethods, Parser $parser, Container $container) + public function __construct(Parser $parser, Container $container) { - $this->descendIntoOtherMethods = $descendIntoOtherMethods; $this->parser = $parser; $this->container = $container; } @@ -56,10 +50,6 @@ public function __construct(bool $descendIntoOtherMethods, Parser $parser, Conta */ public function findQueryBuilderTypesInCalledMethod(Scope $scope, MethodReflection $methodReflection): array { - if (!$this->descendIntoOtherMethods) { - return []; - } - $methodName = $methodReflection->getName(); $className = $methodReflection->getDeclaringClass()->getName(); $fileName = $methodReflection->getDeclaringClass()->getFileName(); diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetDqlDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetDqlDynamicReturnTypeExtension.php index 3b7dfb19..69ceb824 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetDqlDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetDqlDynamicReturnTypeExtension.php @@ -13,9 +13,12 @@ class QueryBuilderGetDqlDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - /** @var string|null */ - private $queryBuilderClass; + /** @var class-string|null */ + private ?string $queryBuilderClass = null; + /** + * @param class-string|null $queryBuilderClass + */ public function __construct( ?string $queryBuilderClass ) @@ -41,7 +44,7 @@ public function getTypeFromMethodCall( { $type = $scope->getType(new MethodCall( new MethodCall($methodCall->var, new Identifier('getQuery')), - new Identifier('getDQL') + new Identifier('getDQL'), )); return TypeCombinator::removeNull($type); diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php index 366eaa60..98fdefb3 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php @@ -55,24 +55,22 @@ class QueryBuilderGetQueryDynamicReturnTypeExtension implements DynamicMethodRet 'orhaving', ]; - /** @var ObjectMetadataResolver */ - private $objectMetadataResolver; + private ObjectMetadataResolver $objectMetadataResolver; - /** @var ArgumentsProcessor */ - private $argumentsProcessor; + private ArgumentsProcessor $argumentsProcessor; - /** @var string|null */ - private $queryBuilderClass; + /** @var class-string|null */ + private ?string $queryBuilderClass = null; - /** @var DescriptorRegistry */ - private $descriptorRegistry; + private DescriptorRegistry $descriptorRegistry; - /** @var PhpVersion */ - private $phpVersion; + private PhpVersion $phpVersion; - /** @var DriverDetector */ - private $driverDetector; + private DriverDetector $driverDetector; + /** + * @param class-string|null $queryBuilderClass + */ public function __construct( ObjectMetadataResolver $objectMetadataResolver, ArgumentsProcessor $argumentsProcessor, diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php index cb76ba29..0c6d9266 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderMethodDynamicReturnTypeExtension.php @@ -7,7 +7,6 @@ use PhpParser\Node\Identifier; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Doctrine\DoctrineTypeUtils; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\MixedType; @@ -23,9 +22,12 @@ class QueryBuilderMethodDynamicReturnTypeExtension implements DynamicMethodRetur private const MAX_COMBINATIONS = 16; - /** @var string|null */ - private $queryBuilderClass; + /** @var class-string|null */ + private ?string $queryBuilderClass = null; + /** + * @param class-string|null $queryBuilderClass + */ public function __construct( ?string $queryBuilderClass ) @@ -40,9 +42,7 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - $returnType = ParametersAcceptorSelector::selectSingle( - $methodReflection->getVariants() - )->getReturnType(); + $returnType = $methodReflection->getVariants()[0]->getReturnType(); if ($returnType instanceof MixedType) { return false; } diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderType.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderType.php index 729cda17..f707888c 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderType.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderType.php @@ -14,7 +14,7 @@ abstract class QueryBuilderType extends ObjectType { /** @var array */ - private $methodCalls = []; + private array $methodCalls = []; final public function __construct( string $className, diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php index 4b72f650..66b18f71 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderTypeSpecifyingExtension.php @@ -24,12 +24,14 @@ class QueryBuilderTypeSpecifyingExtension implements MethodTypeSpecifyingExtensi private const MAX_COMBINATIONS = 16; - /** @var string|null */ - private $queryBuilderClass; + /** @var class-string|null */ + private ?string $queryBuilderClass = null; - /** @var TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; + /** + * @param class-string|null $queryBuilderClass + */ public function __construct(?string $queryBuilderClass) { $this->queryBuilderClass = $queryBuilderClass; @@ -62,7 +64,7 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod $returnType = ParametersAcceptorSelector::selectFromArgs( $scope, $node->getArgs(), - $methodReflection->getVariants() + $methodReflection->getVariants(), )->getReturnType(); if ($returnType instanceof MixedType) { return new SpecifiedTypes([]); @@ -100,8 +102,8 @@ public function specifyTypes(MethodReflection $methodReflection, MethodCall $nod $queryBuilderNode, TypeCombinator::union(...$resultTypes), TypeSpecifierContext::createTruthy(), - true - ); + $scope, + )->setAlwaysOverwriteTypes(); } } diff --git a/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php b/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php index 2f780f22..5f308ba1 100644 --- a/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php +++ b/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php @@ -23,8 +23,7 @@ class ReturnQueryBuilderExpressionTypeResolverExtension implements ExpressionTypeResolverExtension { - /** @var OtherMethodQueryBuilderParser */ - private $otherMethodQueryBuilderParser; + private OtherMethodQueryBuilderParser $otherMethodQueryBuilderParser; public function __construct( OtherMethodQueryBuilderParser $otherMethodQueryBuilderParser diff --git a/src/Type/Doctrine/QueryBuilder/SimpleQueryBuilderType.php b/src/Type/Doctrine/QueryBuilder/SimpleQueryBuilderType.php deleted file mode 100644 index 98660de8..00000000 --- a/src/Type/Doctrine/QueryBuilder/SimpleQueryBuilderType.php +++ /dev/null @@ -1,33 +0,0 @@ -getMethodCalls()) === count($type->getMethodCalls()); - } - - return parent::equals($type); - } - - public function isSuperTypeOf(Type $type): TrinaryLogic - { - if ($type instanceof parent) { - $thisCount = count($this->getMethodCalls()); - $thatCount = count($type->getMethodCalls()); - - return TrinaryLogic::createFromBoolean($thisCount === $thatCount); - } - - return parent::isSuperTypeOf($type); - } - -} diff --git a/stubs/Collections/ArrayCollection.stub b/stubs/Collections/ArrayCollection.stub index 2ecb9ea6..34d07450 100644 --- a/stubs/Collections/ArrayCollection.stub +++ b/stubs/Collections/ArrayCollection.stub @@ -2,6 +2,8 @@ namespace Doctrine\Common\Collections; +use Closure; + /** * @template TKey of array-key * @template T @@ -11,4 +13,33 @@ namespace Doctrine\Common\Collections; class ArrayCollection implements Collection, Selectable { + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(T, TKey):bool $p + * + * @return static + */ + public function filter(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(T):U $func + * + * @return static + * + * @template U + */ + public function map(Closure $func); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return array{0: static, 1: static} + */ + public function partition(Closure $p); + } diff --git a/stubs/Collections/Collection.stub b/stubs/Collections/Collection.stub index be162ef6..05139edd 100644 --- a/stubs/Collections/Collection.stub +++ b/stubs/Collections/Collection.stub @@ -3,6 +3,7 @@ namespace Doctrine\Common\Collections; use ArrayAccess; +use Closure; use Countable; use IteratorAggregate; @@ -43,4 +44,33 @@ interface Collection extends Countable, IteratorAggregate, ArrayAccess, Readable */ public function removeElement($element) {} + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(T, TKey):bool $p + * + * @return Collection + */ + public function filter(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(T):U $func + * + * @return Collection + * + * @template U + */ + public function map(Closure $func); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return array{0: Collection, 1: Collection} + */ + public function partition(Closure $p); + } diff --git a/stubs/Collections/Collection1.stub b/stubs/Collections/Collection1.stub index 455733c8..0f5ad708 100644 --- a/stubs/Collections/Collection1.stub +++ b/stubs/Collections/Collection1.stub @@ -3,6 +3,7 @@ namespace Doctrine\Common\Collections; use ArrayAccess; +use Closure; use Countable; use IteratorAggregate; @@ -43,4 +44,33 @@ interface Collection extends Countable, IteratorAggregate, ArrayAccess, Readable */ public function removeElement($element) {} + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(T, TKey):bool $p + * + * @return Collection + */ + public function filter(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(T):U $func + * + * @return Collection + * + * @template U + */ + public function map(Closure $func); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return array{0: Collection, 1: Collection} + */ + public function partition(Closure $p); + } diff --git a/stubs/Collections/ReadableCollection.stub b/stubs/Collections/ReadableCollection.stub index df07c526..d9fba5ae 100644 --- a/stubs/Collections/ReadableCollection.stub +++ b/stubs/Collections/ReadableCollection.stub @@ -2,6 +2,7 @@ namespace Doctrine\Common\Collections; +use Closure; use Countable; use IteratorAggregate; @@ -13,4 +14,73 @@ use IteratorAggregate; interface ReadableCollection extends Countable, IteratorAggregate { + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return bool + */ + public function exists(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(T, TKey):bool $p + * + * @return ReadableCollection + */ + public function filter(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(T):U $func + * + * @return ReadableCollection + * + * @template U + */ + public function map(Closure $func); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return array{0: ReadableCollection, 1: ReadableCollection} + */ + public function partition(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return bool TRUE, if the predicate yields TRUE for all elements, FALSE otherwise. + */ + public function forAll(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return T|null + */ + public function findFirst(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(TReturn|TInitial, T):TReturn $func + * @param TInitial $initial + * + * @return TReturn|TInitial + * + * @template TReturn + * @template TInitial + */ + public function reduce(Closure $func, mixed $initial = null); + } diff --git a/stubs/Collections/ReadableCollection1.stub b/stubs/Collections/ReadableCollection1.stub new file mode 100644 index 00000000..dec73af0 --- /dev/null +++ b/stubs/Collections/ReadableCollection1.stub @@ -0,0 +1,86 @@ + + */ +interface ReadableCollection extends Countable, IteratorAggregate +{ + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return bool + */ + public function exists(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(T, TKey):bool $p + * + * @return ReadableCollection + */ + public function filter(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(T):U $func + * + * @return Collection + * + * @template U + */ + public function map(Closure $func); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return array{0: ReadableCollection, 1: ReadableCollection} + */ + public function partition(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return bool TRUE, if the predicate yields TRUE for all elements, FALSE otherwise. + */ + public function forAll(Closure $p); + + /** + * @param-immediately-invoked-callable $p + * + * @param Closure(TKey, T):bool $p + * + * @return T|null + */ + public function findFirst(Closure $p); + + /** + * @param-immediately-invoked-callable $func + * + * @param Closure(TReturn|TInitial, T):TReturn $func + * @param TInitial $initial + * + * @return TReturn|TInitial + * + * @template TReturn + * @template TInitial + */ + public function reduce(Closure $func, mixed $initial = null); + +} diff --git a/stubs/bleedingEdge/DBAL/ArrayParameterType.stub b/stubs/DBAL/ArrayParameterType.stub similarity index 100% rename from stubs/bleedingEdge/DBAL/ArrayParameterType.stub rename to stubs/DBAL/ArrayParameterType.stub diff --git a/stubs/DBAL/Connection.stub b/stubs/DBAL/Connection.stub index 8e88410a..15c8e6e1 100644 --- a/stubs/DBAL/Connection.stub +++ b/stubs/DBAL/Connection.stub @@ -2,7 +2,76 @@ namespace Doctrine\DBAL; +use Closure; +use Doctrine\DBAL\Cache\CacheException; +use Doctrine\DBAL\Cache\QueryCacheProfile; +use Doctrine\DBAL\Types\Type; +use Throwable; + class Connection { + /** + * Executes an SQL statement with the given parameters and returns the number of affected rows. + * + * Could be used for: + * - DML statements: INSERT, UPDATE, DELETE, etc. + * - DDL statements: CREATE, DROP, ALTER, etc. + * - DCL statements: GRANT, REVOKE, etc. + * - Session control statements: ALTER SESSION, SET, DECLARE, etc. + * - Other statements that don't yield a row set. + * + * This method supports PDO binding types as well as DBAL mapping types. + * + * @param __doctrine-literal-string $sql SQL statement + * @param list|array $params Statement parameters + * @param array|array $types Parameter types + * + * @return int|string The number of affected rows. + * + * @throws Exception + */ + public function executeStatement($sql, array $params = [], array $types = []); + + /** + * Executes an, optionally parameterized, SQL query. + * + * If the query is parametrized, a prepared statement is used. + * If an SQLLogger is configured, the execution is logged. + * + * @param __doctrine-literal-string $sql SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @throws Exception + */ + public function executeQuery( + string $sql, + array $params = [], + $types = [], + ?QueryCacheProfile $qcp = null + ): Result; + + /** + * Executes a caching query. + * + * @param __doctrine-literal-string $sql SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @throws CacheException + * @throws Exception + */ + public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp): Result; + + /** + * @param-immediately-invoked-callable $func + * @param Closure(self): T $func + * @return T + * + * @template T + * + * @throws Throwable + */ + public function transactional(Closure $func); } diff --git a/stubs/bleedingEdge/DBAL/Connection4.stub b/stubs/DBAL/Connection4.stub similarity index 81% rename from stubs/bleedingEdge/DBAL/Connection4.stub rename to stubs/DBAL/Connection4.stub index 0e6edd58..77b13d78 100644 --- a/stubs/bleedingEdge/DBAL/Connection4.stub +++ b/stubs/DBAL/Connection4.stub @@ -2,9 +2,11 @@ namespace Doctrine\DBAL; +use Closure; use Doctrine\DBAL\Cache\CacheException; use Doctrine\DBAL\Cache\QueryCacheProfile; use Doctrine\DBAL\Types\Type; +use Throwable; /** * @phpstan-type WrapperParameterType = string|Type|ParameterType|ArrayParameterType @@ -24,7 +26,7 @@ class Connection * * This method supports PDO binding types as well as DBAL mapping types. * - * @param literal-string $sql SQL statement + * @param __doctrine-literal-string $sql SQL statement * @param list|array $params Statement parameters * @param WrapperParameterTypeArray $types Parameter types * @@ -40,7 +42,7 @@ class Connection * If the query is parametrized, a prepared statement is used. * If an SQLLogger is configured, the execution is logged. * - * @param literal-string $sql SQL query + * @param __doctrine-literal-string $sql SQL query * @param list|array $params Query parameters * @param WrapperParameterTypeArray $types Parameter types * @@ -56,7 +58,7 @@ class Connection /** * Executes a caching query. * - * @param literal-string $sql SQL query + * @param __doctrine-literal-string $sql SQL query * @param list|array $params Query parameters * @param WrapperParameterTypeArray $types Parameter types * @@ -65,4 +67,15 @@ class Connection */ public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp): Result; + /** + * @param-immediately-invoked-callable $func + * @param Closure(self): T $func + * @return T + * + * @template T + * + * @throws Throwable + */ + public function transactional(Closure $func); + } diff --git a/stubs/bleedingEdge/DBAL/ParameterType.stub b/stubs/DBAL/ParameterType.stub similarity index 100% rename from stubs/bleedingEdge/DBAL/ParameterType.stub rename to stubs/DBAL/ParameterType.stub diff --git a/stubs/EntityRepository.stub b/stubs/EntityRepository.stub index cfa838fd..784b14c8 100644 --- a/stubs/EntityRepository.stub +++ b/stubs/EntityRepository.stub @@ -42,7 +42,7 @@ class EntityRepository implements ObjectRepository * @phpstan-param array|null $orderBy * @phpstan-return TEntityClass|null */ - public function findOneBy(array $criteria, array $orderBy = null); + public function findOneBy(array $criteria, ?array $orderBy = null); /** * @phpstan-return class-string @@ -63,4 +63,19 @@ class EntityRepository implements ObjectRepository */ public function matching(Criteria $criteria); + /** + * @param __doctrine-literal-string $alias + * @param __doctrine-literal-string|null $indexBy + * + * @return QueryBuilder + */ + public function createQueryBuilder($alias, $indexBy = null); + + /** + * @param array $criteria + * + * @return int<0, max> + */ + public function count(array $criteria); + } diff --git a/stubs/MongoClassMetadataInfo.stub b/stubs/MongoClassMetadataInfo.stub index cfdba938..65cfa40f 100644 --- a/stubs/MongoClassMetadataInfo.stub +++ b/stubs/MongoClassMetadataInfo.stub @@ -16,6 +16,7 @@ class ClassMetadata implements BaseClassMetadata public $customRepositoryClassName; /** + * @readonly * @var class-string */ public $name; diff --git a/stubs/ORM/AbstractQuery.stub b/stubs/ORM/AbstractQuery.stub index 1b970ad0..25f7e320 100644 --- a/stubs/ORM/AbstractQuery.stub +++ b/stubs/ORM/AbstractQuery.stub @@ -4,6 +4,7 @@ namespace Doctrine\ORM; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; /** * @template-covariant TKey The type of column used in indexBy @@ -12,21 +13,75 @@ use Doctrine\ORM\NonUniqueResultException; abstract class AbstractQuery { + public const HYDRATE_OBJECT = 1; + /** * @param ArrayCollection|array $parameters * @return static */ public function setParameters($parameters) { - } - /** - * @return bool|float|int|string|null - * - * @throws NoResultException - * @throws NonUniqueResultException - */ - public function getSingleScalarResult(); + /** + * @phpstan-impure + * @param string|AbstractQuery::HYDRATE_* $hydrationMode + */ + public function getResult($hydrationMode = self::HYDRATE_OBJECT): mixed + { + } + + /** + * @phpstan-impure + * @return mixed[] + */ + public function getArrayResult(): array + { + } + + /** + * @phpstan-impure + * @return mixed[] + */ + public function getSingleColumnResult(): array + { + } + + /** + * @phpstan-impure + * @return mixed[] + */ + public function getScalarResult(): array + { + } + + /** + * @phpstan-impure + * @param string|AbstractQuery::HYDRATE_*|null $hydrationMode + * @throws NonUniqueResultException + */ + public function getOneOrNullResult($hydrationMode = null): mixed + { + } + + /** + * @phpstan-impure + * @param string|AbstractQuery::HYDRATE_*|null $hydrationMode + * @throws NonUniqueResultException + * @throws NoResultException + */ + public function getSingleResult($hydrationMode = null): mixed + { + } + + /** + * @phpstan-impure + * @return bool|float|int|string|null + * @throws NoResultException + * @throws NonUniqueResultException + */ + public function getSingleScalarResult() + { + } } diff --git a/stubs/ORM/QueryBuilder.stub b/stubs/ORM/QueryBuilder.stub index db14c5b2..46b8e0ee 100644 --- a/stubs/ORM/QueryBuilder.stub +++ b/stubs/ORM/QueryBuilder.stub @@ -2,6 +2,8 @@ namespace Doctrine\ORM; +use Doctrine\ORM\Query\Expr; + class QueryBuilder { @@ -21,4 +23,136 @@ class QueryBuilder { } + /** + * @param string $dqlPartName + * @param __doctrine-literal-string|object|list<__doctrine-literal-string>|array{join: array} $dqlPart + * @param bool $append + * + * @return $this + */ + public function add($dqlPartName, $dqlPart, $append = false) + { + + } + + /** + * @param __doctrine-literal-string|null $delete + * @param __doctrine-literal-string|null $alias + * + * @return $this + */ + public function delete($delete = null, $alias = null) + { + + } + + /** + * @param __doctrine-literal-string|null $update + * @param __doctrine-literal-string|null $alias + * + * @return $this + */ + public function update($update = null, $alias = null) + { + + } + + /** + * @param __doctrine-literal-string|class-string $from + * @param __doctrine-literal-string $alias + * @param __doctrine-literal-string|null $indexBy + * + * @return $this + */ + public function from($from, $alias, $indexBy = null) + { + + } + + /** + * @param __doctrine-literal-string|class-string $join + * @param __doctrine-literal-string $alias + * @param Expr\Join::ON|Expr\Join::WITH|null $conditionType + * @param __doctrine-literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition + * @param __doctrine-literal-string|null $indexBy + * + * @return $this + */ + public function innerJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null) + { + + } + + /** + * @param __doctrine-literal-string|class-string $join + * @param __doctrine-literal-string $alias + * @param Expr\Join::ON|Expr\Join::WITH|null $conditionType + * @param __doctrine-literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition + * @param __doctrine-literal-string|null $indexBy + * + * @return $this + */ + public function leftJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null) + { + + } + + /** + * @param __doctrine-literal-string|class-string $join + * @param __doctrine-literal-string $alias + * @param Expr\Join::ON|Expr\Join::WITH|null $conditionType + * @param __doctrine-literal-string|Expr\Comparison|Expr\Composite|Expr\Func|null $condition + * @param __doctrine-literal-string|null $indexBy + * + * @return $this + */ + public function join($join, $alias, $conditionType = null, $condition = null, $indexBy = null) + { + + } + + /** + * @return __doctrine-literal-string + */ + public function getRootAlias() + { + + } + + /** + * @return list<__doctrine-literal-string> + */ + public function getRootAliases() + { + + } + + /** + * @return list<__doctrine-literal-string> + */ + public function getAllAlias() + { + + } + + /** + * @param __doctrine-literal-string|object|array $predicates + * @return $this + */ + public function where($predicates) + { + + } + + /** + * @param __doctrine-literal-string|object|array $predicates + * @return $this + */ + public function andWhere($predicates) + { + + } + + + } diff --git a/stubs/bleedingEdge/DBAL/Connection.stub b/stubs/bleedingEdge/DBAL/Connection.stub deleted file mode 100644 index 6bb4a01d..00000000 --- a/stubs/bleedingEdge/DBAL/Connection.stub +++ /dev/null @@ -1,64 +0,0 @@ -|array $params Statement parameters - * @param array|array $types Parameter types - * - * @return int|string The number of affected rows. - * - * @throws Exception - */ - public function executeStatement($sql, array $params = [], array $types = []); - - /** - * Executes an, optionally parameterized, SQL query. - * - * If the query is parametrized, a prepared statement is used. - * If an SQLLogger is configured, the execution is logged. - * - * @param literal-string $sql SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types - * - * @throws Exception - */ - public function executeQuery( - string $sql, - array $params = [], - $types = [], - ?QueryCacheProfile $qcp = null - ): Result; - - /** - * Executes a caching query. - * - * @param literal-string $sql SQL query - * @param list|array $params Query parameters - * @param array|array $types Parameter types - * - * @throws CacheException - * @throws Exception - */ - public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp): Result; - -} diff --git a/stubs/bleedingEdge/EntityRepository.stub b/stubs/bleedingEdge/EntityRepository.stub deleted file mode 100644 index 6e22e323..00000000 --- a/stubs/bleedingEdge/EntityRepository.stub +++ /dev/null @@ -1,74 +0,0 @@ - - */ -class EntityRepository implements ObjectRepository -{ - - /** @var class-string */ - protected $_entityName; - - /** - * @phpstan-param mixed $id - * @phpstan-param int|null $lockMode - * @phpstan-param int|null $lockVersion - * @phpstan-return TEntityClass|null - */ - public function find($id, $lockMode = null, $lockVersion = null); - - /** - * @phpstan-return list - */ - public function findAll(); - - /** - * @phpstan-param array $criteria - * @phpstan-param array|null $orderBy - * @phpstan-param int|null $limit - * @phpstan-param int|null $offset - * @phpstan-return list - */ - public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null); - - /** - * @phpstan-param array $criteria The criteria. - * @phpstan-param array|null $orderBy - * @phpstan-return TEntityClass|null - */ - public function findOneBy(array $criteria, array $orderBy = null); - - /** - * @phpstan-return class-string - */ - public function getClassName(); - - /** - * @phpstan-return class-string - */ - protected function getEntityName(); - - /** - * @param \Doctrine\Common\Collections\Criteria $criteria - * - * @return \Doctrine\Common\Collections\Collection - * - * @psalm-return \Doctrine\Common\Collections\Collection - */ - public function matching(Criteria $criteria); - - /** - * @param literal-string $alias - * @param literal-string|null $indexBy - * - * @return QueryBuilder - */ - public function createQueryBuilder($alias, $indexBy = null); - -} diff --git a/stubs/bleedingEdge/ORM/QueryBuilder.stub b/stubs/bleedingEdge/ORM/QueryBuilder.stub deleted file mode 100644 index 25e05ee2..00000000 --- a/stubs/bleedingEdge/ORM/QueryBuilder.stub +++ /dev/null @@ -1,156 +0,0 @@ - - */ - public function getQuery() - { - } - - /** - * @param \Doctrine\Common\Collections\ArrayCollection|array $parameters - * @return static - */ - public function setParameters($parameters) - { - - } - - /** - * @param string $dqlPartName - * @param literal-string|object|list|array{join: array} $dqlPart - * @param bool $append - * - * @return $this - */ - public function add($dqlPartName, $dqlPart, $append = false) - { - - } - - /** - * @param literal-string|null $delete - * @param literal-string|null $alias - * - * @return $this - */ - public function delete($delete = null, $alias = null) - { - - } - - /** - * @param literal-string|null $update - * @param literal-string|null $alias - * - * @return $this - */ - public function update($update = null, $alias = null) - { - - } - - /** - * @param literal-string|class-string $from - * @param literal-string $alias - * @param literal-string|null $indexBy - * - * @return $this - */ - public function from($from, $alias, $indexBy = null) - { - - } - - /** - * @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 - * @param literal-string|null $indexBy - * - * @return $this - */ - public function innerJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null) - { - - } - - /** - * @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 - * @param literal-string|null $indexBy - * - * @return $this - */ - public function leftJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null) - { - - } - - /** - * @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 - * @param literal-string|null $indexBy - * - * @return $this - */ - public function join($join, $alias, $conditionType = null, $condition = null, $indexBy = null) - { - - } - - /** - * @return literal-string - */ - public function getRootAlias() - { - - } - - /** - * @return list - */ - public function getRootAliases() - { - - } - - /** - * @return list - */ - public function getAllAlias() - { - - } - - /** - * @param literal-string|object|array $predicates - * @return $this - */ - public function where($predicates) - { - - } - - /** - * @param literal-string|object|array $predicates - * @return $this - */ - public function andWhere($predicates) - { - - } - -} diff --git a/tests/BackedEnumStubExtension.php b/tests/BackedEnumStubExtension.php new file mode 100644 index 00000000..80cf36f4 --- /dev/null +++ b/tests/BackedEnumStubExtension.php @@ -0,0 +1,22 @@ += 80100) { + return []; + } + + return [ + __DIR__ . '/../compatibility/BackedEnum.stub', + ]; + } + +} diff --git a/tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php b/tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php index f1f7dd32..7c92d905 100644 --- a/tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php +++ b/tests/Classes/DoctrineProxyForbiddenClassNamesExtensionTest.php @@ -33,7 +33,7 @@ public function testForbiddenClassNameExtension(): void 20, 'This is most likely unintentional. Did you mean to type \TestPhpStanEntity?', ], - ] + ], ); } diff --git a/tests/Classes/entity-manager.php b/tests/Classes/entity-manager.php index a84bdc54..9c0301d1 100644 --- a/tests/Classes/entity-manager.php +++ b/tests/Classes/entity-manager.php @@ -19,13 +19,13 @@ $metadataDriver = new MappingDriverChain(); $metadataDriver->addDriver(new AnnotationDriver( new AnnotationReader(), - [__DIR__ . '/data'] + [__DIR__ . '/data'], ), 'PHPStan\\Rules\\Doctrine\\ORM\\'); if (PHP_VERSION_ID >= 80100) { $metadataDriver->addDriver( new AttributeDriver([__DIR__ . '/data-attributes']), - 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\' + 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\', ); } @@ -33,7 +33,7 @@ Type::overrideType( 'date', - DateTimeImmutableType::class + DateTimeImmutableType::class, ); return new EntityManager( @@ -41,5 +41,5 @@ 'driver' => 'pdo_sqlite', 'memory' => true, ]), - $config + $config, ); diff --git a/tests/DoctrineIntegration/ODM/document-manager.php b/tests/DoctrineIntegration/ODM/document-manager.php index bfdb4141..d104e205 100644 --- a/tests/DoctrineIntegration/ODM/document-manager.php +++ b/tests/DoctrineIntegration/ODM/document-manager.php @@ -16,11 +16,11 @@ $config->setMetadataDriverImpl( new AnnotationDriver( new AnnotationReader(), - [__DIR__ . '/data'] - ) + [__DIR__ . '/data'], + ), ); return DocumentManager::create( null, - $config + $config, ); diff --git a/tests/DoctrineIntegration/ORM/EntityRepositoryDynamicReturnIntegrationTest.php b/tests/DoctrineIntegration/ORM/EntityRepositoryDynamicReturnIntegrationTest.php index e64f096d..de7910fc 100644 --- a/tests/DoctrineIntegration/ORM/EntityRepositoryDynamicReturnIntegrationTest.php +++ b/tests/DoctrineIntegration/ORM/EntityRepositoryDynamicReturnIntegrationTest.php @@ -10,7 +10,7 @@ final class EntityRepositoryDynamicReturnIntegrationTest extends LevelsTestCase /** * @return string[][] */ - public function dataTopics(): array + public static function dataTopics(): array { return [ ['entityRepositoryDynamicReturn'], diff --git a/tests/DoctrineIntegration/ORM/EntityRepositoryWithoutObjectManagerLoaderDynamicReturnIntegrationTest.php b/tests/DoctrineIntegration/ORM/EntityRepositoryWithoutObjectManagerLoaderDynamicReturnIntegrationTest.php index bb2d04c1..ea596a6e 100644 --- a/tests/DoctrineIntegration/ORM/EntityRepositoryWithoutObjectManagerLoaderDynamicReturnIntegrationTest.php +++ b/tests/DoctrineIntegration/ORM/EntityRepositoryWithoutObjectManagerLoaderDynamicReturnIntegrationTest.php @@ -10,7 +10,7 @@ final class EntityRepositoryWithoutObjectManagerLoaderDynamicReturnIntegrationTe /** * @return string[][] */ - public function dataTopics(): array + public static function dataTopics(): array { return [ ['entityRepositoryDynamicReturn'], diff --git a/tests/DoctrineIntegration/ORM/entity-manager.php b/tests/DoctrineIntegration/ORM/entity-manager.php index 23f27436..fb21532a 100644 --- a/tests/DoctrineIntegration/ORM/entity-manager.php +++ b/tests/DoctrineIntegration/ORM/entity-manager.php @@ -15,8 +15,8 @@ $config->setMetadataDriverImpl( new AnnotationDriver( new AnnotationReader(), - [__DIR__ . '/data'] - ) + [__DIR__ . '/data'], + ), ); return new EntityManager( @@ -24,5 +24,5 @@ 'driver' => 'pdo_sqlite', 'memory' => true, ]), - $config + $config, ); diff --git a/tests/DoctrineIntegration/TypeInferenceTest.php b/tests/DoctrineIntegration/TypeInferenceTest.php index 82a4c236..ba2c4fd6 100644 --- a/tests/DoctrineIntegration/TypeInferenceTest.php +++ b/tests/DoctrineIntegration/TypeInferenceTest.php @@ -14,6 +14,7 @@ public function dataFileAsserts(): iterable { yield from $this->gatherAssertTypes(__DIR__ . '/data/getRepository.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/isEmpty.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/Collection.php'); } /** diff --git a/tests/DoctrineIntegration/data/Collection.php b/tests/DoctrineIntegration/data/Collection.php new file mode 100644 index 00000000..f56e69d7 --- /dev/null +++ b/tests/DoctrineIntegration/data/Collection.php @@ -0,0 +1,56 @@ + */ + private $items; + + public function __construct() + { + /** @var ArrayCollection $numbers */ + $numbers = new ArrayCollection([1, 2, 3]); + + $filteredNumbers = $numbers->filter(function (int $number): bool { + return $number % 2 === 1; + }); + assertType('Doctrine\Common\Collections\ArrayCollection', $filteredNumbers); + + $items = $filteredNumbers->map(static function (int $number): Item { + return new Item(); + }); + assertType('Doctrine\Common\Collections\ArrayCollection', $items); + + $this->items = $items; + } + + public function removeOdd(): void + { + $this->items = $this->items->filter(function (Item $item, int $idx): bool { + return $idx % 2 === 1; + }); + assertType('Doctrine\Common\Collections\Collection', $this->items); + } + + public function __clone() + { + $this->items = $this->items->map( + static function (Item $item): Item { + return clone $item; + } + ); + assertType('Doctrine\Common\Collections\Collection', $this->items); + } + +} + +class Item +{ + +} diff --git a/tests/Platform/Entity/PlatformEntity.php b/tests/Platform/Entity/PlatformEntity.php index 6da280e9..efc1f68d 100644 --- a/tests/Platform/Entity/PlatformEntity.php +++ b/tests/Platform/Entity/PlatformEntity.php @@ -17,90 +17,58 @@ class PlatformEntity /** * @ORM\Id * @ORM\Column(type="string",nullable=false) - * @var string */ #[ORM\Id] #[ORM\Column(type: 'string', nullable: false)] - public $id; + public string $id; /** * @ORM\ManyToOne(targetEntity=PlatformRelatedEntity::class) * @ORM\JoinColumn(name="related_entity_id", referencedColumnName="id", nullable=false) - * @var PlatformRelatedEntity */ #[ORM\ManyToOne(targetEntity: PlatformRelatedEntity::class)] #[ORM\JoinColumn(name: 'related_entity_id', referencedColumnName: 'id', nullable: false)] - public $related_entity; + public PlatformRelatedEntity $related_entity; - /** - * @ORM\Column(type="string", name="col_string", nullable=false) - * @var string - */ + /** @ORM\Column(type="string", name="col_string", nullable=false) */ #[ORM\Column(type: 'string', name: 'col_string', nullable: false)] - public $col_string; + public string $col_string; - /** - * @ORM\Column(type="string", name="col_string_nullable", nullable=true) - * @var string|null - */ + /** @ORM\Column(type="string", name="col_string_nullable", nullable=true) */ #[ORM\Column(type: 'string', name: 'col_string_nullable', nullable: true)] - public $col_string_nullable; + public ?string $col_string_nullable = null; - /** - * @ORM\Column(type="boolean", name="col_bool", nullable=false) - * @var bool - */ + /** @ORM\Column(type="boolean", name="col_bool", nullable=false) */ #[ORM\Column(type: 'boolean', name: 'col_bool', nullable: false)] - public $col_bool; + public bool $col_bool; - /** - * @ORM\Column(type="boolean", name="col_bool_nullable", nullable=true) - * @var bool|null - */ + /** @ORM\Column(type="boolean", name="col_bool_nullable", nullable=true) */ #[ORM\Column(type: 'boolean', name: 'col_bool_nullable', nullable: true)] - public $col_bool_nullable; + public ?bool $col_bool_nullable = null; - /** - * @ORM\Column(type="float", name="col_float", nullable=false) - * @var float - */ + /** @ORM\Column(type="float", name="col_float", nullable=false) */ #[ORM\Column(type: 'float', name: 'col_float', nullable: false)] - public $col_float; + public float $col_float; - /** - * @ORM\Column(type="float", name="col_float_nullable", nullable=true) - * @var float|null - */ + /** @ORM\Column(type="float", name="col_float_nullable", nullable=true) */ #[ORM\Column(type: 'float', name: 'col_float_nullable', nullable: true)] - public $col_float_nullable; + public ?float $col_float_nullable = null; - /** - * @ORM\Column(type="decimal", name="col_decimal", nullable=false, scale=1, precision=2) - * @var string - */ + /** @ORM\Column(type="decimal", name="col_decimal", nullable=false, scale=1, precision=2) */ #[ORM\Column(type: 'decimal', name: 'col_decimal', nullable: false, scale: 1, precision: 2)] - public $col_decimal; + public string $col_decimal; - /** - * @ORM\Column(type="decimal", name="col_decimal_nullable", nullable=true, scale=1, precision=2) - * @var string|null - */ + /** @ORM\Column(type="decimal", name="col_decimal_nullable", nullable=true, scale=1, precision=2) */ #[ORM\Column(type: 'decimal', name: 'col_decimal_nullable', nullable: true, scale: 1, precision: 2)] - public $col_decimal_nullable; + public ?string $col_decimal_nullable = null; - /** - * @ORM\Column(type="integer", name="col_int", nullable=false) - * @var int - */ + /** @ORM\Column(type="integer", name="col_int", nullable=false) */ #[ORM\Column(type: 'integer', name: 'col_int', nullable: false)] - public $col_int; + public int $col_int; - /** - * @ORM\Column(type="integer", name="col_int_nullable", nullable=true) - * @var int|null - */ + /** @ORM\Column(type="integer", name="col_int_nullable", nullable=true) */ #[ORM\Column(type: 'integer', name: 'col_int_nullable', nullable: true)] - public $col_int_nullable; + public ?int $col_int_nullable = null; /** * @ORM\Column(type="bigint", name="col_bigint", nullable=false) @@ -123,11 +91,8 @@ class PlatformEntity #[ORM\Column(type: 'mixed', name: 'col_mixed', nullable: false)] public $col_mixed; - /** - * @ORM\Column(type="datetime", name="col_datetime", nullable=false) - * @var DateTimeInterface - */ + /** @ORM\Column(type="datetime", name="col_datetime", nullable=false) */ #[ORM\Column(type: 'datetime', name: 'col_datetime', nullable: false)] - public $col_datetime; + public DateTimeInterface $col_datetime; } diff --git a/tests/Platform/Entity/PlatformRelatedEntity.php b/tests/Platform/Entity/PlatformRelatedEntity.php index 86c4b00a..fec1a0a5 100644 --- a/tests/Platform/Entity/PlatformRelatedEntity.php +++ b/tests/Platform/Entity/PlatformRelatedEntity.php @@ -16,10 +16,9 @@ class PlatformRelatedEntity /** * @ORM\Id * @ORM\Column(type="integer", nullable=false) - * @var int */ #[ORM\Id] #[ORM\Column(type: 'integer', nullable: false)] - public $id; + public int $id; } diff --git a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php index 04fdcba8..48846980 100644 --- a/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php +++ b/tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php @@ -25,7 +25,9 @@ use PHPStan\Platform\Entity\PlatformEntity; use PHPStan\Platform\Entity\PlatformRelatedEntity; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; @@ -71,6 +73,7 @@ final class QueryResultTypeWalkerFetchTypeMatrixTest extends PHPStanTestCase private const STRINGIFY_NONE = 'none'; private const STRINGIFY_DEFAULT = 'default'; private const STRINGIFY_PG_BOOL = 'pg_bool'; + private const STRINGIFY_PG_FLOAT = 'pg_float'; private const CONFIG_DEFAULT = 'default'; private const CONFIG_STRINGIFY = 'pdo_stringify'; @@ -134,7 +137,7 @@ public function testPdoMysqlDefault( PHP_VERSION_ID, $mysqlExpectedType, $mysqlExpectedResult, - $stringify + $stringify, ); } @@ -174,7 +177,7 @@ public function testPdoMysqlStringify( PHP_VERSION_ID, $mysqlExpectedType, $mysqlExpectedResult, - $stringify + $stringify, ); } @@ -214,7 +217,7 @@ public function testPdoMysqlNoEmulate( PHP_VERSION_ID, $mysqlExpectedType, $mysqlExpectedResult, - $stringify + $stringify, ); } @@ -254,7 +257,7 @@ public function testPdoMysqlStringifyNoEmulate( PHP_VERSION_ID, $mysqlExpectedType, $mysqlExpectedResult, - $stringify + $stringify, ); } @@ -294,7 +297,7 @@ public function testPdoMysqliDefault( PHP_VERSION_ID, $mysqlExpectedType, $mysqlExpectedResult, - $stringify + $stringify, ); } @@ -334,7 +337,7 @@ public function testPdoSqliteDefault( PHP_VERSION_ID, $sqliteExpectedType, $sqliteExpectedResult, - $stringify + $stringify, ); } @@ -374,7 +377,7 @@ public function testPdoSqliteStringify( PHP_VERSION_ID, $sqliteExpectedType, $sqliteExpectedResult, - $stringify + $stringify, ); } @@ -414,7 +417,7 @@ public function testPdoSqlite3( PHP_VERSION_ID, $sqliteExpectedType, $sqliteExpectedResult, - $stringify + $stringify, ); } @@ -454,7 +457,7 @@ public function testPdoPgsqlDefault( PHP_VERSION_ID, $pdoPgsqlExpectedType, $pdoPgsqlExpectedResult, - $stringify + $stringify, ); } @@ -494,7 +497,7 @@ public function testPdoPgsqlStringify( PHP_VERSION_ID, $pdoPgsqlExpectedType, $pdoPgsqlExpectedResult, - $stringify + $stringify, ); } @@ -534,7 +537,7 @@ public function testPgsql( PHP_VERSION_ID, $pgsqlExpectedType, $pgsqlExpectedResult, - $stringify + $stringify, ); } @@ -574,7 +577,7 @@ public function testUnsupportedDriver( PHP_VERSION_ID, $mssqlExpectedType, $mssqlExpectedResult, - $stringify + $stringify, ); } @@ -615,7 +618,7 @@ public function testUnknownDriver( $this->determineTypeForUnknownDriverUnknownSetup($mysqlExpectedType, $stringify), $mysqlExpectedResult, $stringify, - true + true, ); } @@ -656,7 +659,7 @@ public function testUnknownDriverStringify( $this->determineTypeForUnknownDriverUnknownSetup($mysqlExpectedType, $stringify), $mysqlExpectedResult, $stringify, - true + true, ); } @@ -924,7 +927,7 @@ public static function provideCases(): iterable yield '1 + 1 * 1 / 1 - 1' => [ 'data' => self::dataDefault(), 'select' => 'SELECT (1 + 1 * 1 / 1 - 1) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), 'pdo_pgsql' => self::int(), 'pgsql' => self::int(), @@ -974,15 +977,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_int + t.col_float FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 9.125, 'sqliteResult' => 9.125, - 'pdoPgsqlResult' => '9.125', + 'pdoPgsqlResult' => 9.125, 'pgsqlResult' => 9.125, 'mssqlResult' => 9.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_int + t.col_mixed' => [ @@ -1006,15 +1009,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_bigint + t.col_float FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 2147483648.125, 'sqliteResult' => 2147483648.125, - 'pdoPgsqlResult' => '2147483648.125', + 'pdoPgsqlResult' => 2147483648.125, 'pgsqlResult' => 2147483648.125, 'mssqlResult' => 2147483648.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_bigint + t.col_float (int data)' => [ @@ -1022,15 +1025,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_bigint + t.col_float FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 2.0, 'sqliteResult' => 2.0, - 'pdoPgsqlResult' => '2', + 'pdoPgsqlResult' => 2.0, 'pgsqlResult' => 2.0, 'mssqlResult' => 2.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_float + t.col_float' => [ @@ -1038,24 +1041,24 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_float + t.col_float FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 0.25, 'sqliteResult' => 0.25, - 'pdoPgsqlResult' => '0.25', + 'pdoPgsqlResult' => 0.25, 'pgsqlResult' => 0.25, 'mssqlResult' => 0.25, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_int + t.col_decimal' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_int + t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '9.1', 'sqliteResult' => 9.1, @@ -1068,10 +1071,10 @@ public static function provideCases(): iterable yield 't.col_int + t.col_decimal (int data)' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT t.col_int + t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '2.0', 'sqliteResult' => 2, @@ -1086,15 +1089,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_float + t.col_decimal FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 0.225, 'sqliteResult' => 0.225, - 'pdoPgsqlResult' => '0.225', + 'pdoPgsqlResult' => 0.225, 'pgsqlResult' => 0.225, 'mssqlResult' => 0.225, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_float + t.col_decimal (int data)' => [ @@ -1102,24 +1105,24 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_float + t.col_decimal FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 2.0, 'sqliteResult' => 2.0, - 'pdoPgsqlResult' => '2', + 'pdoPgsqlResult' => 2.0, 'pgsqlResult' => 2.0, 'mssqlResult' => 2.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_decimal + t.col_decimal (int data)' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT t.col_decimal + t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '2.0', 'sqliteResult' => 2, @@ -1134,24 +1137,24 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_int + t.col_float + t.col_decimal FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 9.225, 'sqliteResult' => 9.225, - 'pdoPgsqlResult' => '9.225', + 'pdoPgsqlResult' => 9.225, 'pgsqlResult' => 9.225, 'mssqlResult' => 9.225, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_decimal + t.col_decimal' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_decimal + t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '0.2', 'sqliteResult' => 0.2, @@ -1228,7 +1231,7 @@ public static function provideCases(): iterable yield 't.col_decimal + t.col_bool' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_decimal + t.col_bool FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), 'pdo_pgsql' => null, // Undefined function 'pgsql' => null, // Undefined function @@ -1276,7 +1279,7 @@ public static function provideCases(): iterable yield 't.col_int / t.col_int' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_int / t.col_int FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), 'pdo_pgsql' => self::int(), 'pgsql' => self::int(), @@ -1292,7 +1295,7 @@ public static function provideCases(): iterable yield 't.col_bigint / t.col_bigint' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_bigint / t.col_bigint FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), 'pdo_pgsql' => self::int(), 'pgsql' => self::int(), @@ -1310,15 +1313,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_int / t.col_float FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 72.0, 'sqliteResult' => 72.0, - 'pdoPgsqlResult' => '72', + 'pdoPgsqlResult' => 72.0, 'pgsqlResult' => 72.0, 'mssqlResult' => 72.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_int / t.col_float / t.col_decimal' => [ @@ -1326,15 +1329,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_int / t.col_float / t.col_decimal FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 720.0, 'sqliteResult' => 720.0, - 'pdoPgsqlResult' => '720', + 'pdoPgsqlResult' => 720.0, 'pgsqlResult' => 720.0, 'mssqlResult' => 720.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_bigint / t.col_float' => [ @@ -1342,15 +1345,15 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_bigint / t.col_float FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 17179869184.0, 'sqliteResult' => 17179869184.0, - 'pdoPgsqlResult' => '17179869184', + 'pdoPgsqlResult' => 17179869184.0, 'pgsqlResult' => 17179869184.0, 'mssqlResult' => 17179869184.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_float / t.col_float' => [ @@ -1358,24 +1361,24 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_float / t.col_float FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_int / t.col_decimal' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_int / t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '90.0000', 'sqliteResult' => 90.0, @@ -1388,10 +1391,10 @@ public static function provideCases(): iterable yield 't.col_int / t.col_decimal (int data)' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT t.col_int / t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0000', 'sqliteResult' => 1, @@ -1406,24 +1409,24 @@ public static function provideCases(): iterable 'select' => 'SELECT t.col_float / t.col_decimal FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.25, 'sqliteResult' => 1.25, - 'pdoPgsqlResult' => '1.25', + 'pdoPgsqlResult' => 1.25, 'pgsqlResult' => 1.25, 'mssqlResult' => 1.25, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_decimal / t.col_decimal' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_decimal / t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.00000', 'sqliteResult' => 1.0, @@ -1436,10 +1439,10 @@ public static function provideCases(): iterable yield 't.col_decimal / t.col_decimal (int data)' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT t.col_decimal / t.col_decimal FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.00000', 'sqliteResult' => 1, @@ -1516,7 +1519,7 @@ public static function provideCases(): iterable yield 't.col_int / t.col_bool' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_int / t.col_bool FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), 'pdo_pgsql' => null, // Undefined function 'pgsql' => null, // Undefined function @@ -1564,7 +1567,7 @@ public static function provideCases(): iterable yield 't.col_decimal / t.col_bool' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_decimal / t.col_bool FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), 'pdo_pgsql' => null, // Undefined function 'pgsql' => null, // Undefined function @@ -1580,7 +1583,7 @@ public static function provideCases(): iterable yield 't.col_decimal / t.col_bool (int data)' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT t.col_decimal / t.col_bool FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), 'pdo_pgsql' => null, // Undefined function 'pgsql' => null, // Undefined function @@ -1628,7 +1631,7 @@ public static function provideCases(): iterable yield 't.col_int / t.col_int_nullable' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_int / t.col_int_nullable FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::intOrNull(), 'pdo_pgsql' => self::intOrNull(), 'pgsql' => self::intOrNull(), @@ -1708,7 +1711,7 @@ public static function provideCases(): iterable yield '1 / 1' => [ 'data' => self::dataDefault(), 'select' => 'SELECT (1 / 1) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), 'pdo_pgsql' => self::int(), 'pgsql' => self::int(), @@ -1724,10 +1727,10 @@ public static function provideCases(): iterable yield '1 / 1.0' => [ 'data' => self::dataDefault(), 'select' => 'SELECT (1 / 1.0) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0000', 'sqliteResult' => 1.0, @@ -1742,8 +1745,8 @@ public static function provideCases(): iterable 'select' => 'SELECT (1 / 1e0) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, @@ -1948,10 +1951,10 @@ public static function provideCases(): iterable yield 'COALESCE(t.col_decimal, t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT COALESCE(t.col_decimal, t.col_decimal) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1, @@ -1961,30 +1964,46 @@ public static function provideCases(): iterable 'stringify' => self::STRINGIFY_DEFAULT, ]; + yield 'COALESCE(t.col_float, t.col_float)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(t.col_float, t.col_float) FROM %s t', + 'mysql' => self::float(), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::float(), + 'pgsql' => self::float(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0.125, + 'sqliteResult' => 0.125, + 'pdoPgsqlResult' => 0.125, + 'pgsqlResult' => 0.125, + 'mssqlResult' => 0.125, + 'stringify' => self::STRINGIFY_PG_FLOAT, + ]; + yield 'COALESCE(t.col_float, t.col_float) + int data' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT COALESCE(t.col_float, t.col_float) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 't.col_decimal' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_decimal FROM %s t', - 'mysql' => self::numericString(), - 'sqlite' => self::numericString(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), - 'mssql' => self::numericString(), + 'mysql' => self::numericString(false, true), + 'sqlite' => self::numericString(false, true), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), + 'mssql' => self::numericString(false, true), 'mysqlResult' => '0.1', 'sqliteResult' => '0.1', 'pdoPgsqlResult' => '0.1', @@ -2012,11 +2031,11 @@ public static function provideCases(): iterable yield 't.col_bigint' => [ 'data' => self::dataDefault(), 'select' => 'SELECT t.col_bigint FROM %s t', - 'mysql' => self::hasDbal4() ? self::int() : self::numericString(), - 'sqlite' => self::hasDbal4() ? self::int() : self::numericString(), - 'pdo_pgsql' => self::hasDbal4() ? self::int() : self::numericString(), - 'pgsql' => self::hasDbal4() ? self::int() : self::numericString(), - 'mssql' => self::hasDbal4() ? self::int() : self::numericString(), + 'mysql' => self::hasDbal4() ? self::int() : self::numericString(true, true), + 'sqlite' => self::hasDbal4() ? self::int() : self::numericString(true, true), + 'pdo_pgsql' => self::hasDbal4() ? self::int() : self::numericString(true, true), + 'pgsql' => self::hasDbal4() ? self::int() : self::numericString(true, true), + 'mssql' => self::hasDbal4() ? self::int() : self::numericString(true, true), 'mysqlResult' => self::hasDbal4() ? 2147483648 : '2147483648', 'sqliteResult' => self::hasDbal4() ? 2147483648 : '2147483648', 'pdoPgsqlResult' => self::hasDbal4() ? 2147483648 : '2147483648', @@ -2046,15 +2065,15 @@ public static function provideCases(): iterable 'select' => 'SELECT AVG(t.col_float) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => 0.125, 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', + 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'AVG(t.col_float) + no data' => [ @@ -2062,7 +2081,7 @@ public static function provideCases(): iterable 'select' => 'SELECT AVG(t.col_float) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => null, @@ -2070,7 +2089,7 @@ public static function provideCases(): iterable 'pdoPgsqlResult' => null, 'pgsqlResult' => null, 'mssqlResult' => null, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'AVG(t.col_float) + GROUP BY' => [ @@ -2078,15 +2097,15 @@ public static function provideCases(): iterable 'select' => 'SELECT AVG(t.col_float) FROM %s t GROUP BY t.col_int', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 0.125, 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', + 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'AVG(t.col_float_nullable) + GROUP BY' => [ @@ -2094,7 +2113,7 @@ public static function provideCases(): iterable 'select' => 'SELECT AVG(t.col_float_nullable) FROM %s t GROUP BY t.col_int', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => null, @@ -2102,16 +2121,16 @@ public static function provideCases(): iterable 'pdoPgsqlResult' => null, 'pgsqlResult' => null, 'mssqlResult' => null, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'AVG(t.col_decimal)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(false, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(false, true), + 'pgsql' => self::numericStringOrNull(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '0.10000', 'sqliteResult' => 0.1, @@ -2124,10 +2143,10 @@ public static function provideCases(): iterable yield 'AVG(t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT AVG(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(false, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(false, true), + 'pgsql' => self::numericStringOrNull(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.00000', 'sqliteResult' => 1.0, @@ -2156,10 +2175,10 @@ public static function provideCases(): iterable yield 'AVG(t.col_int)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(t.col_int) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '9.0000', 'sqliteResult' => 9.0, @@ -2172,7 +2191,7 @@ public static function provideCases(): iterable yield 'AVG(t.col_bool)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(t.col_bool) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), 'pdo_pgsql' => null, // Undefined function 'pgsql' => null, // Undefined function @@ -2204,10 +2223,10 @@ public static function provideCases(): iterable yield 'AVG(1)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(1) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0000', 'sqliteResult' => 1.0, @@ -2284,10 +2303,10 @@ public static function provideCases(): iterable yield 'AVG(1) + GROUP BY' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(1) FROM %s t GROUP BY t.col_int', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0000', 'sqliteResult' => 1.0, @@ -2300,10 +2319,10 @@ public static function provideCases(): iterable yield 'AVG(1.0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(1.0) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.00000', 'sqliteResult' => 1.0, @@ -2316,10 +2335,10 @@ public static function provideCases(): iterable yield 'AVG(1e0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(1.0) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.00000', 'sqliteResult' => 1.0, @@ -2332,10 +2351,10 @@ public static function provideCases(): iterable yield 'AVG(t.col_bigint)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT AVG(t.col_bigint) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '2147483648.0000', 'sqliteResult' => 2147483648.0, @@ -2350,15 +2369,15 @@ public static function provideCases(): iterable 'select' => 'SELECT SUM(t.col_float) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => 0.125, 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', + 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SUM(t.col_float) + no data' => [ @@ -2366,7 +2385,7 @@ public static function provideCases(): iterable 'select' => 'SELECT SUM(t.col_float) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => null, @@ -2374,7 +2393,7 @@ public static function provideCases(): iterable 'pdoPgsqlResult' => null, 'pgsqlResult' => null, 'mssqlResult' => null, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SUM(t.col_float) + GROUP BY' => [ @@ -2382,15 +2401,15 @@ public static function provideCases(): iterable 'select' => 'SELECT SUM(t.col_float) FROM %s t GROUP BY t.col_int', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 0.125, 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', + 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield '1 + -(CASE WHEN MIN(t.col_float) = 0 THEN SUM(t.col_float) ELSE 0 END)' => [ // agg function (causing null) deeply inside AST @@ -2398,24 +2417,24 @@ public static function provideCases(): iterable 'select' => 'SELECT 1 + -(CASE WHEN MIN(t.col_float) = 0 THEN SUM(t.col_float) ELSE 0 END) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SUM(t.col_decimal)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT SUM(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(false, true), 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(false, true), + 'pgsql' => self::numericStringOrNull(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '0.1', 'sqliteResult' => 0.1, @@ -2428,10 +2447,10 @@ public static function provideCases(): iterable yield 'SUM(t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT SUM(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(false, true), 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(false, true), + 'pgsql' => self::numericStringOrNull(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1, @@ -2444,10 +2463,10 @@ public static function provideCases(): iterable yield 'SUM(t.col_int)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT SUM(t.col_int) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), 'mssql' => self::mixed(), 'mysqlResult' => '9', 'sqliteResult' => 9, @@ -2460,10 +2479,10 @@ public static function provideCases(): iterable yield '-SUM(t.col_int)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT -SUM(t.col_int) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), 'mssql' => self::mixed(), 'mysqlResult' => '-9', 'sqliteResult' => -9, @@ -2476,10 +2495,10 @@ public static function provideCases(): iterable yield '-SUM(t.col_int) + no data' => [ 'data' => self::dataNone(), 'select' => 'SELECT -SUM(t.col_int) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), 'mssql' => self::mixed(), 'mysqlResult' => null, 'sqliteResult' => null, @@ -2508,7 +2527,7 @@ public static function provideCases(): iterable yield 'SUM(t.col_bool)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT SUM(t.col_bool) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::intOrNull(), 'pdo_pgsql' => null, // Undefined function 'pgsql' => null, // Undefined function @@ -2604,10 +2623,10 @@ public static function provideCases(): iterable yield 'SUM(1)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT SUM(1) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), 'mssql' => self::mixed(), 'mysqlResult' => '1', 'sqliteResult' => 1, @@ -2620,10 +2639,10 @@ public static function provideCases(): iterable yield 'SUM(1) + GROUP BY' => [ 'data' => self::dataDefault(), 'select' => 'SELECT SUM(1) FROM %s t GROUP BY t.col_int', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), - 'pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(true, true), self::int()), + 'pgsql' => TypeCombinator::union(self::numericString(true, true), self::int()), 'mssql' => self::mixed(), 'mysqlResult' => '1', 'sqliteResult' => 1, @@ -2636,10 +2655,10 @@ public static function provideCases(): iterable yield 'SUM(1.0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT SUM(1.0) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1.0, @@ -2654,8 +2673,8 @@ public static function provideCases(): iterable 'select' => 'SELECT SUM(1e0) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, @@ -2668,10 +2687,10 @@ public static function provideCases(): iterable yield 'SUM(t.col_bigint)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT SUM(t.col_bigint) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::intOrNull(), - 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), - 'pgsql' => TypeCombinator::union(self::numericStringOrNull(), self::intOrNull()), + 'pdo_pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), + 'pgsql' => TypeCombinator::union(self::numericStringOrNull(true, true), self::intOrNull()), 'mssql' => self::mixed(), 'mysqlResult' => '2147483648', 'sqliteResult' => 2147483648, @@ -2686,15 +2705,15 @@ public static function provideCases(): iterable 'select' => 'SELECT MAX(t.col_float) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => 0.125, 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', + 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'MAX(t.col_float) + no data' => [ @@ -2702,7 +2721,7 @@ public static function provideCases(): iterable 'select' => 'SELECT MAX(t.col_float) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => null, @@ -2710,7 +2729,7 @@ public static function provideCases(): iterable 'pdoPgsqlResult' => null, 'pgsqlResult' => null, 'mssqlResult' => null, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'MAX(t.col_float) + GROUP BY' => [ @@ -2718,24 +2737,24 @@ public static function provideCases(): iterable 'select' => 'SELECT MAX(t.col_float) FROM %s t GROUP BY t.col_int', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 0.125, 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', + 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'MAX(t.col_decimal)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT MAX(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(false, true), 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(false, true), + 'pgsql' => self::numericStringOrNull(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '0.1', 'sqliteResult' => 0.1, @@ -2748,10 +2767,10 @@ public static function provideCases(): iterable yield 'MAX(t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT MAX(t.col_decimal) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(false, true), 'sqlite' => self::floatOrIntOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(false, true), + 'pgsql' => self::numericStringOrNull(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1, @@ -2844,10 +2863,10 @@ public static function provideCases(): iterable yield "MAX('1')" => [ 'data' => self::dataDefault(), 'select' => "SELECT MAX('1') FROM %s t", - 'mysql' => self::numericStringOrNull(), - 'sqlite' => self::numericStringOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), + 'sqlite' => self::numericStringOrNull(true, true), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1', 'sqliteResult' => '1', @@ -2860,10 +2879,10 @@ public static function provideCases(): iterable yield "MAX('1.0')" => [ 'data' => self::dataDefault(), 'select' => "SELECT MAX('1.0') FROM %s t", - 'mysql' => self::numericStringOrNull(), - 'sqlite' => self::numericStringOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), + 'sqlite' => self::numericStringOrNull(true, true), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => '1.0', @@ -2908,10 +2927,10 @@ public static function provideCases(): iterable yield 'MAX(1.0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT MAX(1.0) FROM %s t', - 'mysql' => self::numericStringOrNull(), + 'mysql' => self::numericStringOrNull(true, true), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1.0, @@ -2926,8 +2945,8 @@ public static function provideCases(): iterable 'select' => 'SELECT MAX(1e0) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericStringOrNull(), - 'pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::numericStringOrNull(true, true), + 'pgsql' => self::numericStringOrNull(true, true), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, @@ -2958,24 +2977,24 @@ public static function provideCases(): iterable 'select' => 'SELECT ABS(t.col_float) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 0.125, 'sqliteResult' => 0.125, - 'pdoPgsqlResult' => '0.125', + 'pdoPgsqlResult' => 0.125, 'pgsqlResult' => 0.125, 'mssqlResult' => 0.125, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'ABS(t.col_decimal)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT ABS(t.col_decimal) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '0.1', 'sqliteResult' => 0.1, @@ -2988,10 +3007,10 @@ public static function provideCases(): iterable yield 'ABS(t.col_decimal) + int data' => [ 'data' => self::dataAllIntLike(), 'select' => 'SELECT ABS(t.col_decimal) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(false, true), 'sqlite' => self::floatOrInt(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1, @@ -3132,10 +3151,10 @@ public static function provideCases(): iterable yield 'ABS(1.0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT ABS(1.0) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1.0, @@ -3150,8 +3169,8 @@ public static function provideCases(): iterable 'select' => 'SELECT ABS(1e0) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, @@ -3166,15 +3185,15 @@ public static function provideCases(): iterable 'select' => "SELECT ABS('1.0') FROM %s t", 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "ABS('1')" => [ @@ -3182,15 +3201,15 @@ public static function provideCases(): iterable 'select' => "SELECT ABS('1') FROM %s t", 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'ABS(t.col_bigint)' => [ @@ -3614,15 +3633,15 @@ public static function provideCases(): iterable 'select' => 'SELECT SQRT(t.col_float) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SQRT(t.col_decimal)' => [ @@ -3630,8 +3649,8 @@ public static function provideCases(): iterable 'select' => 'SELECT SQRT(t.col_decimal) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(false, true), + 'pgsql' => self::numericString(false, true), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, @@ -3646,15 +3665,15 @@ public static function provideCases(): iterable 'select' => 'SELECT SQRT(t.col_int) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => self::floatOrNull(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 3.0, 'sqliteResult' => 3.0, - 'pdoPgsqlResult' => '3', + 'pdoPgsqlResult' => 3.0, 'pgsqlResult' => 3.0, 'mssqlResult' => 3.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SQRT(t.col_mixed)' => [ @@ -3667,10 +3686,10 @@ public static function provideCases(): iterable 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SQRT(t.col_int_nullable)' => [ @@ -3678,7 +3697,7 @@ public static function provideCases(): iterable 'select' => 'SELECT SQRT(t.col_int_nullable) FROM %s t', 'mysql' => self::floatOrNull(), 'sqlite' => PHP_VERSION_ID >= 80100 && !self::hasDbal4() ? null : self::floatOrNull(), // fails in UDF since PHP 8.1: sqrt(): Passing null to parameter #1 ($num) of type float is deprecated - 'pdo_pgsql' => self::numericStringOrNull(), + 'pdo_pgsql' => self::floatOrNull(), 'pgsql' => self::floatOrNull(), 'mssql' => self::mixed(), 'mysqlResult' => null, @@ -3686,7 +3705,7 @@ public static function provideCases(): iterable 'pdoPgsqlResult' => null, 'pgsqlResult' => null, 'mssqlResult' => null, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'SQRT(-1)' => [ @@ -3710,15 +3729,15 @@ public static function provideCases(): iterable 'select' => 'SELECT SQRT(1) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "SQRT('1')" => [ @@ -3726,15 +3745,15 @@ public static function provideCases(): iterable 'select' => "SELECT SQRT('1') FROM %s t", 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "SQRT('1.0')" => [ @@ -3742,15 +3761,15 @@ public static function provideCases(): iterable 'select' => "SELECT SQRT('1.0') FROM %s t", 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "SQRT('1e0')" => [ @@ -3758,15 +3777,15 @@ public static function provideCases(): iterable 'select' => "SELECT SQRT('1e0') FROM %s t", 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), + 'pdo_pgsql' => self::float(), 'pgsql' => self::float(), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield "SQRT('foo')" => [ @@ -3806,8 +3825,8 @@ public static function provideCases(): iterable 'select' => 'SELECT SQRT(1.0) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, @@ -4028,10 +4047,10 @@ public static function provideCases(): iterable yield 'COALESCE(SUM(t.col_int_nullable), 0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(SUM(t.col_int_nullable), 0) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), - 'pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pdo_pgsql' => TypeCombinator::union(self::numericString(true, true), self::int()), + 'pgsql' => TypeCombinator::union(self::numericString(true, true), self::int()), 'mssql' => self::mixed(), 'mysqlResult' => '0', 'sqliteResult' => 0, @@ -4044,10 +4063,10 @@ public static function provideCases(): iterable yield 'COALESCE(SUM(ABS(t.col_int)), 0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(SUM(ABS(t.col_int)), 0) FROM %s t', - 'mysql' => self::numericString(), + 'mysql' => self::numericString(true, true), 'sqlite' => self::int(), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), 'mssql' => self::mixed(), 'mysqlResult' => '9', 'sqliteResult' => 9, @@ -4124,10 +4143,10 @@ public static function provideCases(): iterable yield "COALESCE(1, '1')" => [ 'data' => self::dataDefault(), 'select' => "SELECT COALESCE(1, '1') FROM %s t", - 'mysql' => self::numericString(), - 'sqlite' => TypeCombinator::union(self::int(), self::numericString()), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'mysql' => self::numericString(true, true), + 'sqlite' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), 'mssql' => self::mixed(), 'mysqlResult' => '1', 'sqliteResult' => 1, @@ -4142,8 +4161,8 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(1, 1.0) FROM %s t', 'mysql' => self::numericString(), 'sqlite' => TypeCombinator::union(self::int(), self::float()), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), 'mssql' => self::mixed(), 'mysqlResult' => '1.0', 'sqliteResult' => 1, @@ -4153,13 +4172,45 @@ public static function provideCases(): iterable 'stringify' => self::STRINGIFY_DEFAULT, ]; + yield 'COALESCE(0, 0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(0, 0) FROM %s t', + 'mysql' => self::int(), + 'sqlite' => self::int(), + 'pdo_pgsql' => self::int(), + 'pgsql' => self::int(), + 'mssql' => self::mixed(), + 'mysqlResult' => 0, + 'sqliteResult' => 0, + 'pdoPgsqlResult' => 0, + 'pgsqlResult' => 0, + 'mssqlResult' => 0, + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + + yield 'COALESCE(1.0, 1.0)' => [ + 'data' => self::dataDefault(), + 'select' => 'SELECT COALESCE(1.0, 1.0) FROM %s t', + 'mysql' => self::numericString(true, true), + 'sqlite' => self::float(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), + 'mssql' => self::mixed(), + 'mysqlResult' => '1.0', + 'sqliteResult' => 1.0, + 'pdoPgsqlResult' => '1.0', + 'pgsqlResult' => '1.0', + 'mssqlResult' => '1.0', + 'stringify' => self::STRINGIFY_DEFAULT, + ]; + yield 'COALESCE(1e0, 1.0)' => [ 'data' => self::dataDefault(), 'select' => 'SELECT COALESCE(1e0, 1.0) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => self::numericString(), + 'pdo_pgsql' => self::numericString(true, true), + 'pgsql' => self::numericString(true, true), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1.0, @@ -4174,8 +4225,8 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(1, 1.0, 1e0) FROM %s t', 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1, @@ -4189,9 +4240,9 @@ public static function provideCases(): iterable 'data' => self::dataDefault(), 'select' => "SELECT COALESCE(1, 1.0, 1e0, '1') FROM %s t", 'mysql' => self::numericString(), - 'sqlite' => TypeCombinator::union(self::float(), self::int(), self::numericString()), - 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString()), - 'pgsql' => TypeCombinator::union(self::int(), self::numericString()), + 'sqlite' => TypeCombinator::union(self::float(), self::int(), self::numericString(true, true)), + 'pdo_pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), + 'pgsql' => TypeCombinator::union(self::int(), self::numericString(true, true)), 'mssql' => self::mixed(), 'mysqlResult' => '1', 'sqliteResult' => 1, @@ -4238,12 +4289,14 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_float_nullable, 0) FROM %s t', 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pdo_pgsql' => PHP_VERSION_ID < 80400 + ? TypeCombinator::union(self::numericString(), self::int()) + : TypeCombinator::union(self::float(), self::int()), 'pgsql' => TypeCombinator::union(self::float(), self::int()), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0, - 'pdoPgsqlResult' => '0', + 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, 'stringify' => self::STRINGIFY_DEFAULT, @@ -4254,15 +4307,15 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_float_nullable, 0.0) FROM %s t', 'mysql' => self::float(), 'sqlite' => self::float(), - 'pdo_pgsql' => self::numericString(), - 'pgsql' => TypeCombinator::union(self::float(), self::numericString()), + 'pdo_pgsql' => TypeCombinator::union(self::float(), self::numericString(false, true)), + 'pgsql' => TypeCombinator::union(self::float(), self::numericString(false, true)), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => '0', + 'pdoPgsqlResult' => 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'COALESCE(t.col_int_nullable, t.col_decimal_nullable, 0)' => [ @@ -4286,12 +4339,14 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0) FROM %s t', 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pdo_pgsql' => PHP_VERSION_ID < 80400 + ? TypeCombinator::union(self::numericString(), self::int()) + : TypeCombinator::union(self::numericString(), self::int(), self::float()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0, - 'pdoPgsqlResult' => '0', + 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, 'stringify' => self::STRINGIFY_DEFAULT, @@ -4302,12 +4357,14 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0.0) FROM %s t', 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pdo_pgsql' => PHP_VERSION_ID < 80400 + ? TypeCombinator::union(self::numericString(), self::int()) + : TypeCombinator::union(self::numericString(), self::int(), self::float()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => '0', + 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, 'stringify' => self::STRINGIFY_DEFAULT, @@ -4318,12 +4375,14 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, 0e0) FROM %s t', 'mysql' => self::float(), 'sqlite' => TypeCombinator::union(self::float(), self::int()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pdo_pgsql' => PHP_VERSION_ID < 80400 + ? TypeCombinator::union(self::numericString(), self::int()) + : TypeCombinator::union(self::numericString(), self::int(), self::float()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'mssql' => self::mixed(), 'mysqlResult' => 0.0, 'sqliteResult' => 0.0, - 'pdoPgsqlResult' => '0', + 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, 'stringify' => self::STRINGIFY_DEFAULT, @@ -4334,12 +4393,14 @@ public static function provideCases(): iterable 'select' => 'SELECT COALESCE(t.col_int_nullable, t.col_decimal_nullable, t.col_float_nullable, \'0\') FROM %s t', 'mysql' => self::numericString(), 'sqlite' => TypeCombinator::union(self::float(), self::int(), self::numericString()), - 'pdo_pgsql' => TypeCombinator::union(self::numericString(), self::int()), + 'pdo_pgsql' => PHP_VERSION_ID < 80400 + ? TypeCombinator::union(self::numericString(), self::int()) + : TypeCombinator::union(self::numericString(), self::int(), self::float()), 'pgsql' => TypeCombinator::union(self::numericString(), self::int(), self::float()), 'mssql' => self::mixed(), 'mysqlResult' => '0', 'sqliteResult' => '0', - 'pdoPgsqlResult' => '0', + 'pdoPgsqlResult' => PHP_VERSION_ID < 80400 ? '0' : 0.0, 'pgsqlResult' => 0.0, 'mssqlResult' => 0.0, 'stringify' => self::STRINGIFY_DEFAULT, @@ -4371,10 +4432,10 @@ public static function provideCases(): iterable 'mssql' => self::mixed(), 'mysqlResult' => 1.0, 'sqliteResult' => 1, - 'pdoPgsqlResult' => '1', + 'pdoPgsqlResult' => 1.0, 'pgsqlResult' => 1.0, 'mssqlResult' => 1.0, - 'stringify' => self::STRINGIFY_DEFAULT, + 'stringify' => self::STRINGIFY_PG_FLOAT, ]; yield 'COALESCE(t.col_string_nullable, t.col_int)' => [ @@ -4469,7 +4530,7 @@ private function performDriverTest( $dql, $sql, $realResultType->describe(VerbosityLevel::precise()), - $inferredType->describe(VerbosityLevel::precise()) + $inferredType->describe(VerbosityLevel::precise()), )); } @@ -4575,7 +4636,7 @@ private function getInferredType(Query $query): Type $typeBuilder, self::getContainer()->getByType(DescriptorRegistry::class), $phpVersion, - new DriverDetector() + new DriverDetector(), ); return $typeBuilder->getResultType(); @@ -4617,8 +4678,8 @@ private function assertRealResultMatchesExpected( $dataset, $dql, $humanReadablePhpVersion, - $realFirstResult - ) + $realFirstResult, + ), ); } @@ -4634,8 +4695,8 @@ private function assertRealResultMatchesExpected( $sql, $humanReadablePhpVersion, $realFirstResult, - $expectedFirstResultExported - ) + $expectedFirstResultExported, + ), ); } @@ -4669,8 +4730,8 @@ private function assertRealResultMatchesInferred( $this->getHumanReadablePhpVersion($phpVersion), $realFirstResult, $inferredType->describe(VerbosityLevel::precise()), - $realType->describe(VerbosityLevel::precise()) - ) + $realType->describe(VerbosityLevel::precise()), + ), ); } @@ -4696,7 +4757,7 @@ private function assertInferredResultMatchesExpected( $inferredFirstItemType = $inferredType->getFirstIterableValueType(); self::assertTrue( - $inferredFirstItemType->equals($expectedFirstItemType), + $expectedFirstItemType->accepts($inferredFirstItemType, true)->yes(), sprintf( "Mismatch between inferred result and expected type\n\nDriver: %s\nConfig: %s\nDataset: %s\nDQL: %s\nSQL: %s\nPHP: %s\nReal first result: %s\nFirst item inferred as: %s\nFirst item expected type: %s\n", $driver, @@ -4707,8 +4768,8 @@ private function assertInferredResultMatchesExpected( $this->getHumanReadablePhpVersion($phpVersion), $realFirstResult, $inferredFirstItemType->describe(VerbosityLevel::precise()), - $expectedFirstItemType->describe(VerbosityLevel::precise()) - ) + $expectedFirstItemType->describe(VerbosityLevel::precise()), + ), ); } @@ -4783,12 +4844,20 @@ private static function boolOrNull(): Type return TypeCombinator::addNull(new BooleanType()); } - private static function numericString(): Type + private static function numericString(bool $lowercase = false, bool $uppercase = false): Type { - return new IntersectionType([ + $types = [ new StringType(), new AccessoryNumericStringType(), - ]); + ]; + if ($lowercase) { + $types[] = new AccessoryLowercaseStringType(); + } + if ($uppercase) { + $types[] = new AccessoryUppercaseStringType(); + } + + return new IntersectionType($types); } private static function string(): Type @@ -4796,12 +4865,9 @@ private static function string(): Type return new StringType(); } - private static function numericStringOrNull(): Type + private static function numericStringOrNull(bool $lowercase = false, bool $uppercase = false): Type { - return TypeCombinator::addNull(new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ])); + return TypeCombinator::addNull(self::numericString($lowercase, $uppercase)); } private static function int(): Type @@ -4996,6 +5062,15 @@ private function resolveDefaultBooleanStringification(?string $driver, int $php, return $this->resolveDefaultStringification($driver, $php, $configName); } + private function resolveDefaultFloatStringification(?string $driver, int $php, string $configName): bool + { + if ($php < 80400 && $driver === DriverDetector::PDO_PGSQL) { + return true; // pdo_pgsql does stringify floats even without ATTR_STRINGIFY_FETCHES prior to PHP 8.4 + } + + return $this->resolveDefaultStringification($driver, $php, $configName); + } + private function getHumanReadablePhpVersion(int $phpVersion): string { return floor($phpVersion / 10000) . '.' . floor(($phpVersion % 10000) / 100); @@ -5024,6 +5099,10 @@ private function shouldStringify(string $stringification, ?string $driverType, i return $this->resolveDefaultBooleanStringification($driverType, $phpVersion, $configName); } + if ($stringification === self::STRINGIFY_PG_FLOAT) { + return $this->resolveDefaultFloatStringification($driverType, $phpVersion, $configName); + } + throw new LogicException('Unknown stringification: ' . $stringification); } diff --git a/tests/Platform/README.md b/tests/Platform/README.md index d3678117..06d8b843 100644 --- a/tests/Platform/README.md +++ b/tests/Platform/README.md @@ -8,18 +8,22 @@ Set current working directory to project root. - `printf "UID=$(id -u)\nGID=$(id -g)" > .env` - `docker-compose -f tests/Platform/docker/docker-compose.yml up -d` -# Test behaviour with old stringification +# Test behaviour for PHP 8.0 (old stringification) - `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php80 composer update` - `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php80 php -d memory_limit=1G vendor/bin/phpunit --group=platform` -# Test behaviour with new stringification +# Test behaviour for PHP 8.1 (adjusted stringification) - `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 composer update` - `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 php -d memory_limit=1G vendor/bin/phpunit --group=platform` + +# Test behaviour for PHP 8.4 (pdo_pgsql float stringification fix) +- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php84 composer update` +- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php84 php -d memory_limit=1G vendor/bin/phpunit --group=platform` ``` You can also run utilize those containers for PHPStorm PHPUnit configuration. Since the dataset is huge and takes few minutes to run, you can filter only functions you are interested in: ```sh -docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 php -d memory_limit=1G vendor/bin/phpunit --group=platform --filter "AVG" +docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php84 php -d memory_limit=1G vendor/bin/phpunit --group=platform --filter "AVG" ``` diff --git a/tests/Platform/data/config.neon b/tests/Platform/data/config.neon index 38f26ed7..e6dd2808 100644 --- a/tests/Platform/data/config.neon +++ b/tests/Platform/data/config.neon @@ -1,5 +1,2 @@ includes: - ../../../extension.neon -parameters: - featureToggles: - listType: true diff --git a/tests/Platform/docker/Dockerfile84 b/tests/Platform/docker/Dockerfile84 new file mode 100644 index 00000000..30b47ed2 --- /dev/null +++ b/tests/Platform/docker/Dockerfile84 @@ -0,0 +1,19 @@ +FROM php:8.4-cli + +# MSSQL +RUN apt update \ + && apt install -y gnupg2 \ + && apt install -y unixodbc-dev unixodbc \ + && curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \ + && curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | tee /etc/apt/sources.list.d/mssql-tools.list \ + && apt update \ + && ACCEPT_EULA=Y apt install -y msodbcsql17 \ + && pecl install sqlsrv \ + && pecl install pdo_sqlsrv \ + && docker-php-ext-enable sqlsrv pdo_sqlsrv + +COPY ./docker-setup.sh /opt/src/scripts/setup.sh +RUN /opt/src/scripts/setup.sh + +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + diff --git a/tests/Platform/docker/docker-compose.yml b/tests/Platform/docker/docker-compose.yml index 73596b72..4a3b0f48 100644 --- a/tests/Platform/docker/docker-compose.yml +++ b/tests/Platform/docker/docker-compose.yml @@ -63,3 +63,17 @@ services: user: ${UID:-1000}:${GID:-1000} volumes: - ../../../:/app + + php84: + depends_on: [mysql, pgsql] + build: + context: . + dockerfile: ./Dockerfile84 + environment: + MYSQL_HOST: mysql + PGSQL_HOST: pgsql + MSSQL_HOST: mssql + working_dir: /app + user: ${UID:-1000}:${GID:-1000} + volumes: + - ../../../:/app diff --git a/tests/Reflection/Doctrine/DoctrineSelectableClassReflectionExtensionTest.php b/tests/Reflection/Doctrine/DoctrineSelectableClassReflectionExtensionTest.php index 1debe15e..61c90e4f 100644 --- a/tests/Reflection/Doctrine/DoctrineSelectableClassReflectionExtensionTest.php +++ b/tests/Reflection/Doctrine/DoctrineSelectableClassReflectionExtensionTest.php @@ -9,11 +9,9 @@ final class DoctrineSelectableClassReflectionExtensionTest extends PHPStanTestCase { - /** @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; - /** @var DoctrineSelectableClassReflectionExtension */ - private $extension; + private DoctrineSelectableClassReflectionExtension $extension; protected function setUp(): void { diff --git a/tests/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php b/tests/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php index 8c582618..e7a88b28 100644 --- a/tests/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php +++ b/tests/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php @@ -27,10 +27,6 @@ public static function getAdditionalConfigFiles(): array public function testRule(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/unused-private-property.php'], [ [ 'Property PHPStan\Rules\Doctrine\ORM\UnusedPrivateProperty\EntityWithAGeneratedId::$unused is never written, only read.', diff --git a/tests/Rules/DeadCode/entity-manager.php b/tests/Rules/DeadCode/entity-manager.php index bc0d5ccb..30eeec97 100644 --- a/tests/Rules/DeadCode/entity-manager.php +++ b/tests/Rules/DeadCode/entity-manager.php @@ -17,12 +17,12 @@ $metadataDriver = new MappingDriverChain(); $metadataDriver->addDriver(new AnnotationDriver( new AnnotationReader(), - [__DIR__ . '/data'] + [__DIR__ . '/data'], ), 'PHPStan\\Rules\\Doctrine\\ORM\\'); if (PHP_VERSION_ID >= 80100) { $metadataDriver->addDriver( new AttributeDriver([__DIR__ . '/data']), - 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\' + 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\', ); } @@ -33,5 +33,5 @@ 'driver' => 'pdo_sqlite', 'memory' => true, ]), - $config + $config, ); diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 5dfcc639..f0a799e6 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -35,11 +35,9 @@ class EntityColumnRuleTest extends RuleTestCase { - /** @var bool */ - private $allowNullablePropertyForRequiredField; + private bool $allowNullablePropertyForRequiredField; - /** @var string|null */ - private $objectManagerLoader; + private ?string $objectManagerLoader = null; protected function getRule(): Rule { @@ -77,15 +75,14 @@ protected function getRule(): Rule new StringType(), new SimpleArrayType(), new UuidTypeDescriptor(FakeTestingUuidType::class), - new ReflectionDescriptor(CarbonImmutableType::class, $this->createBroker(), self::getContainer()), - new ReflectionDescriptor(CarbonType::class, $this->createBroker(), self::getContainer()), - new ReflectionDescriptor(CustomType::class, $this->createBroker(), self::getContainer()), - new ReflectionDescriptor(CustomNumericType::class, $this->createBroker(), self::getContainer()), + new ReflectionDescriptor(CarbonImmutableType::class, $this->createReflectionProvider(), self::getContainer()), + new ReflectionDescriptor(CarbonType::class, $this->createReflectionProvider(), self::getContainer()), + new ReflectionDescriptor(CustomType::class, $this->createReflectionProvider(), self::getContainer()), + new ReflectionDescriptor(CustomNumericType::class, $this->createReflectionProvider(), self::getContainer()), ]), $this->createReflectionProvider(), true, $this->allowNullablePropertyForRequiredField, - true ); } @@ -162,7 +159,7 @@ public function testRule(?string $objectManagerLoader): void 156, ], [ - 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$invalidSimpleArray type mapping mismatch: database can contain array but property expects array.', + 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$invalidSimpleArray type mapping mismatch: database can contain list but property expects array.', 162, ], [ @@ -233,7 +230,7 @@ public function testRuleWithAllowedNullableProperty(?string $objectManagerLoader 156, ], [ - 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$invalidSimpleArray type mapping mismatch: database can contain array but property expects array.', + 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$invalidSimpleArray type mapping mismatch: database can contain list but property expects array.', 162, ], [ @@ -391,15 +388,27 @@ public function testEnumType(?string $objectManagerLoader): void $this->analyse([__DIR__ . '/data-attributes/enum-type.php'], [ [ 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type2 type mapping mismatch: database can contain PHPStan\Rules\Doctrine\ORMAttributes\FooEnum but property expects PHPStan\Rules\Doctrine\ORMAttributes\BarEnum.', - 35, + 42, ], [ 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type2 type mapping mismatch: property can contain PHPStan\Rules\Doctrine\ORMAttributes\BarEnum but database expects PHPStan\Rules\Doctrine\ORMAttributes\FooEnum.', - 35, + 42, ], [ 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type3 type mapping mismatch: backing type string of enum PHPStan\Rules\Doctrine\ORMAttributes\FooEnum does not match database type int.', - 38, + 45, + ], + [ + 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type5 type mapping mismatch: database can contain list but property expects PHPStan\Rules\Doctrine\ORMAttributes\FooEnum.', + 51, + ], + [ + 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type5 type mapping mismatch: property can contain PHPStan\Rules\Doctrine\ORMAttributes\FooEnum but database expects array.', + 51, + ], + [ + 'Property PHPStan\Rules\Doctrine\ORMAttributes\Foo::$type7 type mapping mismatch: backing type int of enum PHPStan\Rules\Doctrine\ORMAttributes\BazEnum does not match value type string of the database type array.', + 63, ], ]); } diff --git a/tests/Rules/Doctrine/ORM/EntityConstructorNotFinalRuleTest.php b/tests/Rules/Doctrine/ORM/EntityConstructorNotFinalRuleTest.php index 7647704b..a94fdfde 100644 --- a/tests/Rules/Doctrine/ORM/EntityConstructorNotFinalRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityConstructorNotFinalRuleTest.php @@ -13,13 +13,12 @@ class EntityConstructorNotFinalRuleTest extends RuleTestCase { - /** @var string|null */ - private $objectManagerLoader; + private ?string $objectManagerLoader = null; protected function getRule(): Rule { return new EntityConstructorNotFinalRule( - new ObjectMetadataResolver($this->objectManagerLoader, __DIR__ . '/../../../../tmp') + new ObjectMetadataResolver($this->objectManagerLoader, __DIR__ . '/../../../../tmp'), ); } diff --git a/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php b/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php index 26a4e744..65b8f209 100644 --- a/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityMappingExceptionRuleTest.php @@ -16,7 +16,7 @@ class EntityMappingExceptionRuleTest extends RuleTestCase protected function getRule(): Rule { return new EntityMappingExceptionRule( - new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', __DIR__ . '/../../../../tmp') + new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', __DIR__ . '/../../../../tmp'), ); } diff --git a/tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php b/tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php index 9f1494a7..d3f05395 100644 --- a/tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityNotFinalRuleTest.php @@ -13,13 +13,12 @@ class EntityNotFinalRuleTest extends RuleTestCase { - /** @var string|null */ - private $objectManagerLoader; + private ?string $objectManagerLoader = null; protected function getRule(): Rule { return new EntityNotFinalRule( - new ObjectMetadataResolver($this->objectManagerLoader, __DIR__ . '/../../../../tmp') + new ObjectMetadataResolver($this->objectManagerLoader, __DIR__ . '/../../../../tmp'), ); } diff --git a/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php b/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php index cd83bebe..7e81545f 100644 --- a/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityRelationRuleTest.php @@ -14,18 +14,15 @@ class EntityRelationRuleTest extends RuleTestCase { - /** @var bool */ - private $allowNullablePropertyForRequiredField; + private bool $allowNullablePropertyForRequiredField; - /** @var string|null */ - private $objectManagerLoader; + private ?string $objectManagerLoader = null; protected function getRule(): Rule { return new EntityRelationRule( new ObjectMetadataResolver($this->objectManagerLoader, __DIR__ . '/../../../../tmp'), $this->allowNullablePropertyForRequiredField, - true ); } diff --git a/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php b/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php index fcb28f7e..f6255563 100644 --- a/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php +++ b/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php @@ -47,6 +47,7 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform): ?str return null; } + /** @throws ConversionException */ return (string) $value; } diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php deleted file mode 100644 index adad87ee..00000000 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleSlowTest.php +++ /dev/null @@ -1,151 +0,0 @@ - - */ -class QueryBuilderDqlRuleSlowTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new QueryBuilderDqlRule( - new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', __DIR__ . '/../../../../tmp'), - true - ); - } - - public function testRule(): void - { - if (PHP_VERSION_ID < 70300) { - self::markTestSkipped('For some reason PHP 7.2 cannot recover from Trying to get property value of non-object'); - } - $this->analyse([__DIR__ . '/data/query-builder-dql.php'], [ - [ - "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)", - 31, - ], - [ - "QueryBuilder: [Syntax Error] line 0, col 68: Error: Expected end of string, got ')'\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE e.id = :id)", - 43, - ], - [ - "QueryBuilder: [Syntax Error] line 0, col 68: Error: Expected end of string, got ')'\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE e.id = :id)", - 55, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 60 near \'transient = \': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named transient', - 62, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 14 near \'Foo e\': Error: Class \'Foo\' is not defined.', - 71, - ], - [ - 'Could not analyse QueryBuilder with dynamic arguments.', - 99, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 60 near \'transient = \': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named transient', - 107, - ], - [ - "QueryBuilder: [Syntax Error] line 0, col 82: Error: Expected end of string, got ')'\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE e.id = 1 ORDER BY e.name) ASC", - 129, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 78 near \'name ASC\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named name', - 139, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 78 near \'name ASC\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named name', - 160, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 60 near \'transient = 1\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named transient', - 170, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 72 near \'nickname LIKE\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named nickname', - 194, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 72 near \'nickname IS \': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named nickname', - 206, - ], - [ - "QueryBuilder: [Syntax Error] line 0, col 80: Error: Expected =, <, <=, <>, >, >=, !=, got ')'\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE e.id = 1 OR e.nickname) IS NULL", - 218, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 60 near \'transient = \': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named transient', - 234, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 60 near \'nonexistent =\': Error: Class PHPStan\Rules\Doctrine\ORM\MyEntity has no field or association named nonexistent', - 251, - ], - [ - "QueryBuilder: [Syntax Error] line 0, col -1: Error: Expected =, <, <=, <>, >, >=, !=, got end of string.\nDQL: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE foo", - 281, - ], - ]); - } - - public function testRuleBranches(): void - { - $errors = [ - [ - 'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1\': Error: \'p\' is not defined.', - 31, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1\': Error: \'p\' is not defined.', - 45, - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 58 near \'p.id = 1\': Error: \'p\' is not defined.', - 59, - 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e WHERE p.id = 1', - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 93 near \'t.id = 1\': Error: \'t\' is not defined.', - 90, - 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e INNER JOIN e.parent p WHERE p.id = 1 AND t.id = 1', - ], - [ - '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, - 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e INNER JOIN e.parent p WHERE p.id = 1 AND e.foo = 1', - ], - [ - 'QueryBuilder: [Semantical Error] line 0, col 93 near \'t.id = 1\': Error: \'t\' is not defined.', - 107, - 'Detected from DQL branch: SELECT e FROM PHPStan\Rules\Doctrine\ORM\MyEntity e INNER JOIN e.parent p WHERE p.id = 1 AND t.id = 1', - ], - ]; - $this->analyse([__DIR__ . '/data/query-builder-branches-dql.php'], $errors); - } - - public static function getAdditionalConfigFiles(): array - { - return [ - __DIR__ . '/../../../../extension.neon', - __DIR__ . '/entity-manager.neon', - __DIR__ . '/slow.neon', - ]; - } - - protected function shouldFailOnPhpErrors(): bool - { - // doctrine/orm/src/Query/Parser.php throws assert($peek !== null) failed - return false; - } - -} diff --git a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php index ec3dafb7..65fd38d6 100644 --- a/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php +++ b/tests/Rules/Doctrine/ORM/QueryBuilderDqlRuleTest.php @@ -5,7 +5,6 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Doctrine\ObjectMetadataResolver; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -17,16 +16,12 @@ protected function getRule(): Rule { return new QueryBuilderDqlRule( new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', __DIR__ . '/../../../../tmp'), - true + true, ); } public function testRule(): void { - if (PHP_VERSION_ID < 70300) { - self::markTestSkipped('For some reason PHP 7.2 cannot recover from Trying to get property value of non-object'); - } - $this->analyse([__DIR__ . '/data/query-builder-dql.php'], [ [ "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)", diff --git a/tests/Rules/Doctrine/ORM/data-attributes/enum-type.php b/tests/Rules/Doctrine/ORM/data-attributes/enum-type.php index a0c3b937..50e0a2ec 100644 --- a/tests/Rules/Doctrine/ORM/data-attributes/enum-type.php +++ b/tests/Rules/Doctrine/ORM/data-attributes/enum-type.php @@ -18,6 +18,13 @@ enum BarEnum: string { } +enum BazEnum: int { + + case ONE = 1; + case TWO = 2; + +} + #[ORM\Entity] class Foo { @@ -40,4 +47,18 @@ class Foo #[ORM\Column] public FooEnum $type4; + #[ORM\Column(type: "simple_array", enumType: FooEnum::class)] + public FooEnum $type5; + + /** + * @var list + */ + #[ORM\Column(type: "simple_array", enumType: FooEnum::class)] + public array $type6; + + /** + * @var list + */ + #[ORM\Column(type: "simple_array", enumType: BazEnum::class)] + public array $type7; } diff --git a/tests/Rules/Doctrine/ORM/data/query-builder-dql.php b/tests/Rules/Doctrine/ORM/data/query-builder-dql.php index 49aa95f2..e221f3ed 100644 --- a/tests/Rules/Doctrine/ORM/data/query-builder-dql.php +++ b/tests/Rules/Doctrine/ORM/data/query-builder-dql.php @@ -291,6 +291,19 @@ public function qbExprMethod(): void $queryBuilder->getQuery(); } + public function bug602(array $objectConditions, bool $rand): void + { + $orParts = ['e.title LIKE :termLike']; + if ($rand) { + $orParts[] = 'p.version = :term'; + } + $queryBuilder = $this->entityManager->createQueryBuilder(); + $queryBuilder->select('e') + ->from(MyEntity::class, 'e') + ->andWhere($queryBuilder->expr()->orX(...$orParts)) + ->setParameter('termLike', 'someTerm'); + } + } class CustomExpr extends \Doctrine\ORM\Query\Expr diff --git a/tests/Rules/Doctrine/ORM/entity-manager.php b/tests/Rules/Doctrine/ORM/entity-manager.php index 9181f8b8..500a2029 100644 --- a/tests/Rules/Doctrine/ORM/entity-manager.php +++ b/tests/Rules/Doctrine/ORM/entity-manager.php @@ -19,13 +19,13 @@ $metadataDriver = new MappingDriverChain(); $metadataDriver->addDriver(new AnnotationDriver( new AnnotationReader(), - [__DIR__ . '/data'] + [__DIR__ . '/data'], ), 'PHPStan\\Rules\\Doctrine\\ORM\\'); if (PHP_VERSION_ID >= 80100) { $metadataDriver->addDriver( new AttributeDriver([__DIR__ . '/data-attributes']), - 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\' + 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\', ); } @@ -33,7 +33,7 @@ Type::overrideType( 'date', - DateTimeImmutableType::class + DateTimeImmutableType::class, ); return new EntityManager( @@ -41,5 +41,5 @@ 'driver' => 'pdo_sqlite', 'memory' => true, ]), - $config + $config, ); diff --git a/tests/Rules/Doctrine/ORM/slow.neon b/tests/Rules/Doctrine/ORM/slow.neon deleted file mode 100644 index bfda5b8a..00000000 --- a/tests/Rules/Doctrine/ORM/slow.neon +++ /dev/null @@ -1,3 +0,0 @@ -parameters: - doctrine: - queryBuilderFastAlgorithm: false diff --git a/tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php b/tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php index 683ddff3..7b66451e 100644 --- a/tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php +++ b/tests/Rules/Properties/MissingGedmoByPhpDocPropertyAssignRuleTest.php @@ -7,7 +7,6 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Doctrine\ObjectMetadataResolver; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -34,10 +33,6 @@ public static function getAdditionalConfigFiles(): array public function testRule(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/gedmo-property-assign-phpdoc.php'], [ // No errors expected ]); diff --git a/tests/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php index 2f42a9a4..8a72494f 100644 --- a/tests/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -4,7 +4,6 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -24,10 +23,6 @@ public static function getAdditionalConfigFiles(): array public function testRule(): void { - if (PHP_VERSION_ID < 70400) { - self::markTestSkipped('Test requires PHP 7.4.'); - } - $this->analyse([__DIR__ . '/data/missing-readonly-property-assign-phpdoc.php'], [ [ 'Class MissingReadOnlyPropertyAssignPhpDoc\EntityWithAGeneratedId has an uninitialized @readonly property $unassigned. Assign it in the constructor.', diff --git a/tests/Rules/Properties/entity-manager.php b/tests/Rules/Properties/entity-manager.php index 99f7a07e..f36730f8 100644 --- a/tests/Rules/Properties/entity-manager.php +++ b/tests/Rules/Properties/entity-manager.php @@ -17,13 +17,13 @@ $metadataDriver = new MappingDriverChain(); $metadataDriver->addDriver(new AnnotationDriver( new AnnotationReader(), - [__DIR__ . '/data'] + [__DIR__ . '/data'], ), 'PHPStan\\Rules\\Doctrine\\ORM\\'); if (PHP_VERSION_ID >= 80100) { $metadataDriver->addDriver( new AttributeDriver([__DIR__ . '/data']), - 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\' + 'PHPStan\\Rules\\Doctrine\\ORMAttributes\\', ); } @@ -34,5 +34,5 @@ 'driver' => 'pdo_sqlite', 'memory' => true, ]), - $config + $config, ); diff --git a/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php b/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php new file mode 100644 index 00000000..89ac5eb6 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/MysqliResultRowCountReturnTypeTest.php @@ -0,0 +1,42 @@ + */ + public function dataFileAsserts(): iterable + { + $versionParser = new VersionParser(); + if (InstalledVersions::satisfies($versionParser, 'doctrine/dbal', '>=4.0')) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli-result-row-count-dbal-3.php'); + } + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + /** @return string[] */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/mysqli.neon']; + } + +} diff --git a/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php b/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php new file mode 100644 index 00000000..0b6aa6bc --- /dev/null +++ b/tests/Type/Doctrine/DBAL/PDOResultRowCountReturnTypeTest.php @@ -0,0 +1,35 @@ + */ + public function dataFileAsserts(): iterable + { + yield from $this->gatherAssertTypes(__DIR__ . '/data/pdo-result-row-count.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + /** @return string[] */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/pdo.neon']; + } + +} diff --git a/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-3.php b/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-3.php new file mode 100644 index 00000000..8226f6e4 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count-dbal-3.php @@ -0,0 +1,15 @@ +rowCount()); +}; + +function (DriverResult $r): void { + assertType('int', $r->rowCount()); +}; diff --git a/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count.php b/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count.php new file mode 100644 index 00000000..84f69543 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/data/mysqli-result-row-count.php @@ -0,0 +1,15 @@ +rowCount()); +}; + +function (DriverResult $r): void { + assertType('int|numeric-string', $r->rowCount()); +}; diff --git a/tests/Type/Doctrine/DBAL/data/pdo-result-row-count.php b/tests/Type/Doctrine/DBAL/data/pdo-result-row-count.php new file mode 100644 index 00000000..ec73a00c --- /dev/null +++ b/tests/Type/Doctrine/DBAL/data/pdo-result-row-count.php @@ -0,0 +1,15 @@ +rowCount()); +}; + +function (DriverResult $r): void { + assertType('int', $r->rowCount()); +}; diff --git a/tests/Type/Doctrine/DBAL/mysqli.neon b/tests/Type/Doctrine/DBAL/mysqli.neon new file mode 100644 index 00000000..e287719f --- /dev/null +++ b/tests/Type/Doctrine/DBAL/mysqli.neon @@ -0,0 +1,6 @@ +includes: + - ../../../../extension.neon + +parameters: + doctrine: + objectManagerLoader: mysqli.php diff --git a/tests/Type/Doctrine/DBAL/mysqli.php b/tests/Type/Doctrine/DBAL/mysqli.php new file mode 100644 index 00000000..ce3859e5 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/mysqli.php @@ -0,0 +1,25 @@ +setProxyDir(__DIR__); +$config->setProxyNamespace('App\GeneratedProxy'); +$config->setMetadataCache(new ArrayCachePool()); +$config->setMetadataDriverImpl(new AnnotationDriver( + new AnnotationReader(), + [__DIR__ . '/data'], +)); + +return new EntityManager( + DriverManager::getConnection([ + 'driver' => 'mysqli', + 'memory' => true, + ]), + $config, +); diff --git a/tests/Type/Doctrine/DBAL/pdo.neon b/tests/Type/Doctrine/DBAL/pdo.neon new file mode 100644 index 00000000..ee4897c8 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/pdo.neon @@ -0,0 +1,6 @@ +includes: + - ../../../../extension.neon + +parameters: + doctrine: + objectManagerLoader: pdo.php diff --git a/tests/Type/Doctrine/DBAL/pdo.php b/tests/Type/Doctrine/DBAL/pdo.php new file mode 100644 index 00000000..7b6b0da3 --- /dev/null +++ b/tests/Type/Doctrine/DBAL/pdo.php @@ -0,0 +1,25 @@ +setProxyDir(__DIR__); +$config->setProxyNamespace('App\GeneratedProxy'); +$config->setMetadataCache(new ArrayCachePool()); +$config->setMetadataDriverImpl(new AnnotationDriver( + new AnnotationReader(), + [__DIR__ . '/data'], +)); + +return new EntityManager( + DriverManager::getConnection([ + 'driver' => 'pdo_pgsql', + 'memory' => true, + ]), + $config, +); diff --git a/tests/Type/Doctrine/DoctrineSelectableDynamicReturnTypeExtensionTest.php b/tests/Type/Doctrine/DoctrineSelectableDynamicReturnTypeExtensionTest.php index a34c7b22..01b66d7b 100644 --- a/tests/Type/Doctrine/DoctrineSelectableDynamicReturnTypeExtensionTest.php +++ b/tests/Type/Doctrine/DoctrineSelectableDynamicReturnTypeExtensionTest.php @@ -15,8 +15,7 @@ final class DoctrineSelectableDynamicReturnTypeExtensionTest extends TestCase { - /** @var DoctrineSelectableDynamicReturnTypeExtension */ - private $extension; + private DoctrineSelectableDynamicReturnTypeExtension $extension; protected function setUp(): void { @@ -52,10 +51,8 @@ public function testGetTypeFromMethodCall(): void $scope = $this->createMock(Scope::class); $scope->method('getType')->will( self::returnCallback( - static function (): Type { - return new ObjectType(Collection::class); - } - ) + static fn (): Type => new ObjectType(Collection::class), + ), ); $var = $this->createMock(Expr::class); diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php index b9d02c85..11cf5016 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerHydrationModeTest.php @@ -11,7 +11,9 @@ use PHPStan\Php\PhpVersion; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; @@ -79,7 +81,7 @@ public function test(Type $expectedType, string $dql, string $methodName, ?int $ $typeBuilder, self::getContainer()->getByType(DescriptorRegistry::class), self::getContainer()->getByType(PhpVersion::class), - self::getContainer()->getByType(DriverDetector::class) + self::getContainer()->getByType(DriverDetector::class), ); $resolver = self::getContainer()->getByType(HydrationModeReturnTypeResolver::class); @@ -89,12 +91,12 @@ public function test(Type $expectedType, string $dql, string $methodName, ?int $ new ConstantIntegerType($this->getRealHydrationMode($methodName, $hydrationMode)), $typeBuilder->getIndexType(), $typeBuilder->getResultType(), - $entityManager + $entityManager, ) ?? new MixedType(); self::assertSame( $expectedType->describe(VerbosityLevel::precise()), - $type->describe(VerbosityLevel::precise()) + $type->describe(VerbosityLevel::precise()), ); $query = $entityManager->createQuery($dql); @@ -106,8 +108,8 @@ public function test(Type $expectedType, string $dql, string $methodName, ?int $ sprintf( "The inferred type\n%s\nshould accept actual type\n%s", $type->describe(VerbosityLevel::precise()), - $resultType->describe(VerbosityLevel::precise()) - ) + $resultType->describe(VerbosityLevel::precise()), + ), ); } @@ -116,8 +118,6 @@ public function test(Type $expectedType, string $dql, string $methodName, ?int $ */ public static function getTestData(): iterable { - AccessoryArrayListType::setListTypeEnabled(true); - yield 'getResult(object), full entity' => [ self::list(new ObjectType(Simple::class)), ' @@ -140,7 +140,7 @@ public static function getTestData(): iterable yield 'getResult(object), fields' => [ self::list(self::constantArray([ - [new ConstantStringType('decimalColumn'), self::numericString()], + [new ConstantStringType('decimalColumn'), self::numericString(false, true)], [new ConstantStringType('floatColumn'), new FloatType()], ])), ' @@ -176,7 +176,7 @@ public static function getTestData(): iterable yield 'toIterable(object), fields' => [ new IterableType(new IntegerType(), self::constantArray([ - [new ConstantStringType('decimalColumn'), self::numericString()], + [new ConstantStringType('decimalColumn'), self::numericString(false, true)], [new ConstantStringType('floatColumn'), new FloatType()], ])), ' @@ -305,15 +305,23 @@ private static function constantArray(array $elements): Type private static function list(Type $values): Type { - return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $values)); + return TypeCombinator::intersect(new ArrayType(new IntegerType(), $values), new AccessoryArrayListType()); } - private static function numericString(): Type + private static function numericString(bool $lowercase = false, bool $uppercase = false): Type { - return new IntersectionType([ + $types = [ new StringType(), new AccessoryNumericStringType(), - ]); + ]; + if ($lowercase) { + $types[] = new AccessoryLowercaseStringType(); + } + if ($uppercase) { + $types[] = new AccessoryUppercaseStringType(); + } + + return new IntersectionType($types); } /** @@ -400,14 +408,14 @@ private static function stringifies(): bool private static function floatOrStringified(): Type { return self::stringifies() - ? self::numericString() + ? self::numericString(false, true) : new FloatType(); } private static function floatOrIntOrStringified(): Type { return self::stringifies() - ? self::numericString() + ? self::numericString(false, true) : TypeCombinator::union(new FloatType(), new IntegerType()); } diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 003a0737..2b8a3c14 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -14,7 +14,11 @@ use PHPStan\Doctrine\Driver\DriverDetector; use PHPStan\Php\PhpVersion; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; +use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -59,11 +63,9 @@ final class QueryResultTypeWalkerTest extends PHPStanTestCase { - /** @var EntityManagerInterface */ - private static $em; + private static EntityManagerInterface $em; - /** @var DescriptorRegistry */ - private $descriptorRegistry; + private DescriptorRegistry $descriptorRegistry; public static function getAdditionalConfigFiles(): array { @@ -181,6 +183,7 @@ public static function setUpBeforeClass(): void $entityWithEnum->stringEnumColumn = StringEnum::A; $entityWithEnum->intEnumColumn = IntEnum::A; $entityWithEnum->intEnumOnStringColumn = IntEnum::A; + $entityWithEnum->stringEnumListColumn = [StringEnum::A, StringEnum::B]; $em->persist($entityWithEnum); } @@ -198,7 +201,7 @@ public function setUp(): void } /** @dataProvider getTestData */ - public function test(Type $expectedType, string $dql, ?string $expectedExceptionMessage = null, ?string $expectedDeprecationMessage = null): void + public function test(Type $expectedType, string $dql, ?string $expectedExceptionMessage = null): void { $em = self::$em; @@ -209,9 +212,6 @@ public function test(Type $expectedType, string $dql, ?string $expectedException if ($expectedExceptionMessage !== null) { $this->expectException(Throwable::class); $this->expectExceptionMessage($expectedExceptionMessage); - } elseif ($expectedDeprecationMessage !== null) { - $this->expectDeprecation(); - $this->expectDeprecationMessage($expectedDeprecationMessage); } QueryResultTypeWalker::walk( @@ -219,14 +219,14 @@ public function test(Type $expectedType, string $dql, ?string $expectedException $typeBuilder, $this->descriptorRegistry, self::getContainer()->getByType(PhpVersion::class), - self::getContainer()->getByType(DriverDetector::class) + self::getContainer()->getByType(DriverDetector::class), ); $type = $typeBuilder->getResultType(); self::assertSame( $expectedType->describe(VerbosityLevel::precise()), - $type->describe(VerbosityLevel::precise()) + $type->describe(VerbosityLevel::precise()), ); // Double-check our expectations @@ -243,8 +243,8 @@ public function test(Type $expectedType, string $dql, ?string $expectedException sprintf( "The inferred type\n%s\nshould accept actual type\n%s", $type->describe(VerbosityLevel::precise()), - $rowType->describe(VerbosityLevel::precise()) - ) + $rowType->describe(VerbosityLevel::precise()), + ), ); } } @@ -290,7 +290,7 @@ public function getTestData(): iterable yield 'arbitrary left join, selected' => [ TypeCombinator::union( new ObjectType(Many::class), - TypeCombinator::addNull(new ObjectType(One::class)) + TypeCombinator::addNull(new ObjectType(One::class)), ), ' SELECT m, o @@ -303,7 +303,7 @@ public function getTestData(): iterable yield 'arbitrary inner join, selected' => [ TypeCombinator::union( new ObjectType(Many::class), - new ObjectType(One::class) + new ObjectType(One::class), ), ' SELECT m, o @@ -320,7 +320,7 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantIntegerType(0), TypeCombinator::addNull(new ObjectType(One::class))], - ]) + ]), ), ' SELECT m AS many, o @@ -337,7 +337,7 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantStringType('one'), TypeCombinator::addNull(new ObjectType(One::class))], - ]) + ]), ), ' SELECT m AS many, o AS one @@ -354,7 +354,7 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantStringType('one'), new ObjectType(One::class)], - ]) + ]), ), ' SELECT m AS many, o AS one @@ -373,9 +373,9 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantIntegerType(0), new ObjectType(One::class)], - [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], + [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString(true, true)], [new ConstantStringType('intColumn'), new IntegerType()], - ]) + ]), ), ' SELECT m, o, m.id, o.intColumn @@ -395,9 +395,9 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantIntegerType(0), new ObjectType(Many::class)], - [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], + [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString(true, true)], [new ConstantStringType('intColumn'), new IntegerType()], - ]) + ]), ), ' SELECT o, m2, m, m.id, o.intColumn @@ -416,9 +416,9 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantStringType('one'), new ObjectType(One::class)], - [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], + [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString(true, true)], [new ConstantStringType('intColumn'), new IntegerType()], - ]) + ]), ), ' SELECT m AS many, o AS one, m.id, o.intColumn @@ -526,7 +526,7 @@ public function getTestData(): iterable yield 'just root entity and scalars' => [ $this->constantArray([ [new ConstantIntegerType(0), new ObjectType(One::class)], - [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], + [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString(true, true)], ]), ' SELECT o, o.id @@ -671,42 +671,42 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( $this->intOrStringified(), - new NullType() + new NullType(), ), ], [ new ConstantIntegerType(2), TypeCombinator::union( $this->intOrStringified(), - new NullType() + new NullType(), ), ], [ new ConstantIntegerType(3), TypeCombinator::union( $this->intOrStringified(), - new NullType() + new NullType(), ), ], [ new ConstantIntegerType(4), TypeCombinator::union( $this->intOrStringified(), - new NullType() + new NullType(), ), ], [ new ConstantIntegerType(5), TypeCombinator::union( $this->floatOrStringified(), - new NullType() + new NullType(), ), ], [ new ConstantIntegerType(6), TypeCombinator::union( $this->floatOrStringified(), - new NullType() + new NullType(), ), ], [ @@ -751,7 +751,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1), - new NullType() + new NullType(), ), ], ]), @@ -767,14 +767,14 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), - $this->intOrStringified() + $this->intOrStringified(), ), ], [ new ConstantIntegerType(2), TypeCombinator::union( new StringType(), - new NullType() + new NullType(), ), ], [ @@ -784,10 +784,10 @@ public function getTestData(): iterable [ new ConstantIntegerType(4), $this->stringifies() - ? $this->numericString() + ? $this->numericString(false, true) : TypeCombinator::union( new IntegerType(), - new FloatType() + new FloatType(), ), ], ]), @@ -806,7 +806,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), - $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0) + $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0), ), ], ]), @@ -826,7 +826,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( new StringType(), - $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0) + $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0), ), ], ]), @@ -846,7 +846,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0), - $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1) + $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1), ), ], ]), @@ -865,7 +865,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), TypeCombinator::union( $this->stringifies() ? new ConstantStringType('0') : new ConstantIntegerType(0), - $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1) + $this->stringifies() ? new ConstantStringType('1') : new ConstantIntegerType(1), ), ], ]), @@ -1080,7 +1080,7 @@ public function getTestData(): iterable new ConstantIntegerType(1), new ObjectType(OneId::class), ], - ]) + ]), ), ' SELECT NEW QueryResult\Entities\ManyId(m.id), @@ -1470,8 +1470,8 @@ public function getTestData(): iterable yield 'unary minus' => [ $this->constantArray([ [new ConstantStringType('minusInt'), $this->stringifies() ? new ConstantStringType('-1') : new ConstantIntegerType(-1)], - [new ConstantStringType('minusFloat'), $this->stringifies() ? $this->numericString() : new ConstantFloatType(-0.1)], - [new ConstantStringType('minusIntRange'), $this->stringifies() ? $this->numericString() : IntegerRangeType::fromInterval(null, 0)], + [new ConstantStringType('minusFloat'), $this->stringifies() ? $this->numericString(false, true) : new ConstantFloatType(-0.1)], + [new ConstantStringType('minusIntRange'), $this->stringifies() ? $this->numericString(true, true) : IntegerRangeType::fromInterval(null, 0)], ]), ' SELECT -1 as minusInt, @@ -1499,9 +1499,10 @@ private function yieldConditionalDataset(): iterable $this->constantArray([ [new ConstantStringType('stringEnumColumn'), new ObjectType(StringEnum::class)], [new ConstantStringType('intEnumColumn'), new ObjectType(IntEnum::class)], + [new ConstantStringType('stringEnumListColumn'), TypeCombinator::intersect(new ArrayType(new IntegerType(), new ObjectType(StringEnum::class)), new AccessoryArrayListType())], ]), ' - SELECT e.stringEnumColumn, e.intEnumColumn + SELECT e.stringEnumColumn, e.intEnumColumn, e.stringEnumListColumn FROM QueryResult\EntitiesEnum\EntityWithEnum e ', ]; @@ -1511,7 +1512,7 @@ private function yieldConditionalDataset(): iterable $this->constantArray([ [ new ConstantIntegerType(1), - new StringType(), + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), ], [ new ConstantIntegerType(2), @@ -1519,7 +1520,7 @@ private function yieldConditionalDataset(): iterable ], [ new ConstantIntegerType(3), - $this->numericString(), + $this->numericString(true, true), ], ]), ' @@ -1531,36 +1532,6 @@ private function yieldConditionalDataset(): iterable ]; } - if (PHP_VERSION_ID >= 70400) { - yield 'locate function' => [ - $this->constantArray([ - [new ConstantIntegerType(1), $this->uintOrStringified()], - [new ConstantIntegerType(2), TypeCombinator::addNull($this->uintOrStringified())], - [new ConstantIntegerType(3), TypeCombinator::addNull($this->uintOrStringified())], - [new ConstantIntegerType(4), $this->uintOrStringified()], - ]), - ' - SELECT LOCATE(m.stringColumn, m.stringColumn, 0), - LOCATE(m.stringNullColumn, m.stringColumn, 0), - LOCATE(m.stringColumn, m.stringNullColumn, 0), - LOCATE(\'f\', \'foo\', 0) - FROM QueryResult\Entities\Many m - ', - null, - InstalledVersions::satisfies(new VersionParser(), 'doctrine/dbal', '>=3.4') - ? null - : ( - PHP_VERSION_ID >= 80100 - ? 'strpos(): Passing null to parameter #2 ($needle) of type string is deprecated' - : ( - PHP_VERSION_ID < 80000 - ? 'strpos(): Non-string needles will be interpreted as strings in the future. Use an explicit chr() call to preserve the current behavior' - : null - ) - ), - ]; - } - $ormVersion = InstalledVersions::getVersion('doctrine/orm'); $hasOrm3 = $ormVersion !== null && strpos($ormVersion, '3.') === 0; @@ -1618,12 +1589,20 @@ private function constantArray(array $elements): Type return $builder->getArray(); } - private function numericString(): Type + private function numericString(bool $lowercase = false, bool $uppercase = false): Type { - return new IntersectionType([ + $types = [ new StringType(), new AccessoryNumericStringType(), - ]); + ]; + if ($lowercase) { + $types[] = new AccessoryLowercaseStringType(); + } + if ($uppercase) { + $types[] = new AccessoryUppercaseStringType(); + } + + return new IntersectionType($types); } private function uint(): Type @@ -1669,21 +1648,21 @@ private function stringifies(): bool private function intOrStringified(): Type { return $this->stringifies() - ? $this->numericString() + ? $this->numericString(true, true) : new IntegerType(); } private function uintOrStringified(): Type { return $this->stringifies() - ? $this->numericString() + ? $this->numericString(true, true) : $this->uint(); } private function floatOrStringified(): Type { return $this->stringifies() - ? $this->numericString() + ? $this->numericString(false, true) : new FloatType(); } diff --git a/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php b/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php index c22acd45..2a4c535a 100644 --- a/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php +++ b/tests/Type/Doctrine/data/QueryResult/EntitiesEnum/EntityWithEnum.php @@ -38,4 +38,10 @@ class EntityWithEnum * @Column(type="string", enumType="QueryResult\EntitiesEnum\IntEnum") */ public $intEnumOnStringColumn; + + /** + * @var list + * @Column(type="simple_array", enumType="QueryResult\EntitiesEnum\StringEnum") + */ + public $stringEnumListColumn; } diff --git a/tests/Type/Doctrine/data/QueryResult/config.neon b/tests/Type/Doctrine/data/QueryResult/config.neon index 5ce3210a..147e4f94 100644 --- a/tests/Type/Doctrine/data/QueryResult/config.neon +++ b/tests/Type/Doctrine/data/QueryResult/config.neon @@ -3,5 +3,3 @@ includes: parameters: doctrine: objectManagerLoader: entity-manager.php - featureToggles: - listType: true diff --git a/tests/Type/Doctrine/data/QueryResult/queryResult.php b/tests/Type/Doctrine/data/QueryResult/queryResult.php index de4cc5bf..54eef205 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryResult.php +++ b/tests/Type/Doctrine/data/QueryResult/queryResult.php @@ -169,7 +169,7 @@ public function testReturnTypeOfQueryMethodsWithExplicitArrayHydrationMode(Entit ); assertType( - 'array', + 'array', $query->getArrayResult() ); assertType(