From c639ac86d809eb50260230730077bc8ed26b81ae Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Mon, 12 Oct 2026 20:40:43 +0200 Subject: [PATCH] CakePHP 6 upgrade --- .github/workflows/ci.yml | 21 ++---- .travis.yml | 43 ----------- README.md | 10 ++- composer.json | 21 +++--- phpstan-test-app.neon | 2 - phpunit.xml | 26 +++---- .../ControllerMethodMustBeUsedRule.php | 3 +- src/Rule/Debug/DisallowDebugFuncCallRule.php | 3 +- src/Rule/LoadObjectExistsCakeClassRule.php | 9 ++- src/Rule/Mailer/GetMailerExistsClassRule.php | 3 +- .../AddAssociationMatchOptionsTypesRule.php | 8 ++- ...rmSelectQueryFindMatchOptionsTypesRule.php | 8 ++- src/Traits/BaseCakeRegistryReturnTrait.php | 14 ++-- ...seTraitExpressionTypeResolverExtension.php | 3 +- ...leHelperLoadDynamicReturnTypeExtension.php | 6 +- ...FactoryBuildDynamicReturnTypeExtension.php | 2 +- src/Utility/CakeNameRegistry.php | 20 ++++-- .../AddAssociationSetClassNameVisitor.php | 3 +- ...lectQueryFindMatchOptionsTypesRuleTest.php | 12 ++-- .../TableGetMatchOptionsTypesRuleTest.php | 10 +-- tests/TestCase/Type/TypeInferenceTest.php | 37 ++++++++++ tests/data/StaticOnlyTable.php | 10 +++ tests/data/registry-types.php | 71 +++++++++++++++++++ tests/phpstan.neon | 2 + .../Command/MyTestExtendedCommand.php | 9 ++- tests/test_app/Command/MyTestLoadCommand.php | 12 ++-- .../Helper/BazBazHelper.php | 6 +- .../Helper/MyHeadingHelper.php | 8 +-- .../Component/TableAwareComponent.php | 12 ++++ tests/test_app/Mailer/MyTestLoadMailer.php | 2 +- tests/test_app/Model/Table/NotesTable.php | 2 +- tests/test_app/View/Cell/MyTestLoadCell.php | 2 +- 32 files changed, 252 insertions(+), 148 deletions(-) delete mode 100644 .travis.yml create mode 100644 tests/TestCase/Type/TypeInferenceTest.php create mode 100644 tests/data/StaticOnlyTable.php create mode 100644 tests/data/registry-types.php create mode 100644 tests/phpstan.neon rename tests/test_app/{Command => Console}/Helper/BazBazHelper.php (84%) rename tests/test_app/{Command => Console}/Helper/MyHeadingHelper.php (81%) create mode 100644 tests/test_app/Controller/Component/TableAwareComponent.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b45c7a..9f86b41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: branches: - 'master' - - '3.next-cake5' + - '4.next-cake5' pull_request: branches: - '*' @@ -19,18 +19,11 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['8.1', '8.4'] - dependencies: ['highest'] - include: - - php-version: '8.1' - dependencies: 'lowest' - - php-version: '8.2' - dependencies: 'highest' - - php-version: '8.3' - dependencies: 'highest' + php-version: ['8.5'] + dependencies: ['highest', 'lowest'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -47,19 +40,19 @@ jobs: composer-options: ${{ matrix.composer-options }} - name: Run PHPUnit - run: vendor/bin/phpunit + run: composer test cs-stan: name: Coding Standard & Static Analysis runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: '8.5' extensions: mbstring, intl coverage: none tools: phive, cs2pr diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5d8cb3f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -language: php - -dist: xenial - -php: - - 7.2 - - 7.3 - -sudo: false - -cache: - directories: - - vendor - - $HOME/.composer/cache - -env: - global: - - DEFAULT=1 - -matrix: - fast_finish: true - - include: - - php: 7.3 - env: PHPCS=1 DEFAULT=0 - - - php: 7.3 - env: PHPSTAN=1 DEFAULT=0 - - - php: 7.3 - env: COVERAGE=1 DEFAULT=0' - -before_script: - - composer install --prefer-dist --no-interaction - -script: - - if [[ $DEFAULT = 1 ]]; then composer test; fi - - if [[ $DEFAULT = 1 ]]; then composer stan-integration; fi - - if [[ $PHPCS = 1 ]]; then composer cs-check; fi - - if [[ $PHPSTAN = 1 ]]; then composer stan; fi - -notifications: - email: false diff --git a/README.md b/README.md index 569a4cc..0c793b2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# CakePHP extension for PHPStan +# CakePHP 6 extension for PHPStan ![Build Status](https://github.com/CakeDC/cakephp-phpstan/actions/workflows/ci.yml/badge.svg) [![Downloads](https://poser.pugx.org/CakeDC/cakephp-phpstan/d/total.svg)](https://packagist.org/packages/CakeDC/cakephp-phpstan) @@ -13,6 +13,10 @@ and multiple rules. # Installation +Requires PHP 8.5+, CakePHP 6, and PHPStan 2.1.26+. CakePHP 6 is currently in development; configure `"minimum-stability": "dev"` and `"prefer-stable": true` in the root Composer project to allow its dependencies. + +When upgrading, move custom console helpers to `Console/Helper`. Commands implement `execute(): ?int` and access arguments and console IO through properties. + To use this extension, require it through [Composer](https://getcomposer.org/): ``` @@ -39,11 +43,11 @@ Features included: 1. Provide correct return type for `Cake\ORM\Locator\LocatorInterface::get()` 1. Provide correct return type for `Cake\Controller\Controller::loadComponent()` 1. Provide correct return type for `Cake\Controller\Controller::fetchTable()` -1. Provide correct return type for `Cake\Controller\Component::fetchTable()` +1. Provide correct return type for `fetchTable()` on components using `Cake\ORM\Locator\LocatorAwareTrait` 1. Provide correct return type for `Cake\Command\Command::fetchTable()` 1. Provide correct return type for `Cake\Mailer\Mailer::fetchTable()` 1. Provide correct return type for `Cake\View\Cell::fetchTable()` -1. Provide correct return type for `Cake\Console\ConsoleIo::helper()` +1. Provide correct return type for `Cake\Console\ConsoleIo::helper()` and `Cake\Console\ConsoleIoInterface::helper()` # Table class return type extensions ### SelectQueryFindListReturnTypeExtension diff --git a/composer.json b/composer.json index 8edb7a2..c75d16d 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "cakedc/cakephp-phpstan", - "description": "CakePHP plugin extension for PHPStan.", + "description": "CakePHP 6 plugin extension for PHPStan.", "type": "phpstan-extension", "license": "MIT", "authors": [ @@ -11,14 +11,14 @@ } ], "require": { - "php": ">=8.1.0", + "php": ">=8.5", "phpstan/phpstan": "^2.1.26", - "cakephp/cakephp": "^5.0" + "cakephp/cakephp": "^6.0" }, "require-dev": { "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5 || ^11.5 || ^12.1", - "cakephp/cakephp-codesniffer": "^5.0", + "phpunit/phpunit": "^13.0", + "cakephp/cakephp-codesniffer": "6.x-dev", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-strict-rules": "^2.0" }, @@ -39,12 +39,15 @@ "CakeDC\\PHPStan\\Test\\": "tests/", "CakeDC\\MyPlugin\\": "tests/test_plugin", "App\\": "tests/test_app" - } + }, + "classmap": [ + "tests/data/StaticOnlyTable.php" + ] }, "scripts": { "cs-check": "phpcs -p", "cs-fix": "phpcbf -p", - "test": "phpunit --stderr", + "test": "@php -d memory_limit=1G vendor/bin/phpunit --stderr", "stan-integration": [ "phpstan analyse --debug -c phpstan-test-app.neon", "phpstan analyse --debug -c phpstan-test-plugin.neon" @@ -61,5 +64,7 @@ "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true } - } + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/phpstan-test-app.neon b/phpstan-test-app.neon index 3327966..c250b63 100644 --- a/phpstan-test-app.neon +++ b/phpstan-test-app.neon @@ -11,5 +11,3 @@ parameters: ignoreErrors: - identifier: missingType.generics - - - identifier: method.deprecated diff --git a/phpunit.xml b/phpunit.xml index bbb0f35..dc2988e 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,18 +1,10 @@ - - - - - ./tests - - - - - ./src - - + + + + tests/TestCase + + + + src + diff --git a/src/Rule/Controller/ControllerMethodMustBeUsedRule.php b/src/Rule/Controller/ControllerMethodMustBeUsedRule.php index 680e128..9e1eebc 100644 --- a/src/Rule/Controller/ControllerMethodMustBeUsedRule.php +++ b/src/Rule/Controller/ControllerMethodMustBeUsedRule.php @@ -16,6 +16,7 @@ use Cake\Controller\Controller; use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Identifier; use PhpParser\Node\Stmt\Expression; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; @@ -62,7 +63,7 @@ public function processNode(Node $node, Scope $scope): array $methodCall = $node->expr; // Check if the method name is one we care about - if (!$methodCall->name instanceof Node\Identifier) { + if (!$methodCall->name instanceof Identifier) { return []; } diff --git a/src/Rule/Debug/DisallowDebugFuncCallRule.php b/src/Rule/Debug/DisallowDebugFuncCallRule.php index 9cf4ebb..d897fe8 100644 --- a/src/Rule/Debug/DisallowDebugFuncCallRule.php +++ b/src/Rule/Debug/DisallowDebugFuncCallRule.php @@ -6,6 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -44,7 +45,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { assert($node instanceof FuncCall); - if (!$node->name instanceof Node\Name) { + if (!$node->name instanceof Name) { return []; } $usedName = $node->name->name; diff --git a/src/Rule/LoadObjectExistsCakeClassRule.php b/src/Rule/LoadObjectExistsCakeClassRule.php index 6f4646b..b0edc31 100644 --- a/src/Rule/LoadObjectExistsCakeClassRule.php +++ b/src/Rule/LoadObjectExistsCakeClassRule.php @@ -16,8 +16,11 @@ use CakeDC\PHPStan\Rule\Traits\ParseClassNameFromArgTrait; use PhpParser\Node; use PhpParser\Node\Arg; +use PhpParser\Node\ArrayItem; +use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Identifier; use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; @@ -49,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array { assert($node instanceof MethodCall); $args = $node->getArgs(); - if (!$node->name instanceof Node\Identifier) { + if (!$node->name instanceof Identifier) { return []; } $reference = $scope->getType($node->var)->getReferencedClasses()[0] ?? null; @@ -100,13 +103,13 @@ protected function getInputClassName(String_ $nameArg, ?Arg $options): ?string $className = $nameArg->value; if ( $options === null - || !$options->value instanceof Node\Expr\Array_ + || !$options->value instanceof Array_ ) { return $className; } foreach ($options->value->items as $item) { if ( - !$item instanceof Node\Expr\ArrayItem + !$item instanceof ArrayItem || !$item->key instanceof String_ || $item->key->value !== 'className' ) { diff --git a/src/Rule/Mailer/GetMailerExistsClassRule.php b/src/Rule/Mailer/GetMailerExistsClassRule.php index 9a67ec4..40cf62a 100644 --- a/src/Rule/Mailer/GetMailerExistsClassRule.php +++ b/src/Rule/Mailer/GetMailerExistsClassRule.php @@ -16,6 +16,7 @@ use CakeDC\PHPStan\Utility\CakeNameRegistry; use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Identifier; use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; @@ -56,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array { assert($node instanceof MethodCall); if ( - !$node->name instanceof Node\Identifier + !$node->name instanceof Identifier || $node->name->name !== 'getMailer' ) { return []; diff --git a/src/Rule/Model/AddAssociationMatchOptionsTypesRule.php b/src/Rule/Model/AddAssociationMatchOptionsTypesRule.php index c3f801c..8755154 100644 --- a/src/Rule/Model/AddAssociationMatchOptionsTypesRule.php +++ b/src/Rule/Model/AddAssociationMatchOptionsTypesRule.php @@ -12,7 +12,9 @@ use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\ArrayItem; +use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Identifier; use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; @@ -65,7 +67,7 @@ public function processNode(Node $node, Scope $scope): array { assert($node instanceof MethodCall); $args = $node->getArgs(); - if (!$node->name instanceof Node\Identifier) { + if (!$node->name instanceof Identifier) { return []; } $reference = $scope->getType($node->var)->getReferencedClasses()[0] ?? null; @@ -78,7 +80,7 @@ public function processNode(Node $node, Scope $scope): array } if ( $details['options'] === null - || !$details['options']->value instanceof Node\Expr\Array_ + || !$details['options']->value instanceof Array_ ) { return []; } @@ -170,7 +172,7 @@ protected function processPropertyTypeCheck( $classReflection = $object->getClassReflection(); assert($classReflection instanceof ClassReflection); $propertyType = $classReflection - ->getInstanceProperty('_' . $property, $scope) + ->getInstanceProperty($property, $scope) ->getWritableType(); $assignedValueType = $scope->getType($item->value); $accepts = $this->ruleLevelHelper->accepts($propertyType, $assignedValueType, true); diff --git a/src/Rule/Model/OrmSelectQueryFindMatchOptionsTypesRule.php b/src/Rule/Model/OrmSelectQueryFindMatchOptionsTypesRule.php index a37db68..ccbb936 100644 --- a/src/Rule/Model/OrmSelectQueryFindMatchOptionsTypesRule.php +++ b/src/Rule/Model/OrmSelectQueryFindMatchOptionsTypesRule.php @@ -13,8 +13,10 @@ use CakeDC\PHPStan\Rule\Traits\ParseClassNameFromArgTrait; use InvalidArgumentException; use PhpParser\Node; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Identifier; use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ExtendedMethodReflection; @@ -99,7 +101,7 @@ public function processNode(Node $node, Scope $scope): array { assert($node instanceof MethodCall); $args = $node->getArgs(); - if (!$node->name instanceof Node\Identifier || !in_array($node->name->name, $this->targetMethods, true)) { + if (!$node->name instanceof Identifier || !in_array($node->name->name, $this->targetMethods, true)) { return []; } $referenceClasses = $scope->getType($node->var)->getReferencedClasses(); @@ -238,7 +240,7 @@ protected function getTargetMethod(Scope $scope, string $targetMethod): ?Extende * @param array<\PhpParser\Node\Expr> $options * @return array<\PhpParser\Node\Expr> */ - protected function extractOptionsUnpackedArray(Node\Arg $arg, array $options): array + protected function extractOptionsUnpackedArray(Arg $arg, array $options): array { if ($arg->value instanceof Array_ && $arg->unpack) { $options = $this->getOptionsFromArray($arg->value, $options); @@ -299,7 +301,7 @@ protected function getOptionsFromArray(Array_ $source, array $options): array */ protected function getParamNamesIgnore(Scope $scope, MethodCall $node): array { - assert($node->name instanceof Node\Identifier); + assert($node->name instanceof Identifier); $methodReflection = $scope->getMethodReflection($scope->getType($node->var), $node->name->toString()); if ($methodReflection === null) { return []; diff --git a/src/Traits/BaseCakeRegistryReturnTrait.php b/src/Traits/BaseCakeRegistryReturnTrait.php index 80ae240..72461a0 100644 --- a/src/Traits/BaseCakeRegistryReturnTrait.php +++ b/src/Traits/BaseCakeRegistryReturnTrait.php @@ -18,8 +18,8 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function count; -use function method_exists; trait BaseCakeRegistryReturnTrait { @@ -40,15 +40,15 @@ public function getTypeFromMethodCall( } $argType = $scope->getType($methodCall->getArgs()[0]->value); - if (!method_exists($argType, 'getValue')) { - return new ObjectType($this->defaultClass); - } - $value = $argType->getValue(); - if (!is_string($value)) { + $names = $argType->getConstantStrings(); + if ($names === [] || !TypeCombinator::union(...$names)->isSuperTypeOf($argType)->yes()) { return null; } - return $this->getCakeType($value); + return TypeCombinator::union(...array_map( + fn($name) => $this->getCakeType($name->getValue()), + $names, + )); } /** diff --git a/src/Type/BaseTraitExpressionTypeResolverExtension.php b/src/Type/BaseTraitExpressionTypeResolverExtension.php index a114ba2..754cbf3 100644 --- a/src/Type/BaseTraitExpressionTypeResolverExtension.php +++ b/src/Type/BaseTraitExpressionTypeResolverExtension.php @@ -16,6 +16,7 @@ use CakeDC\PHPStan\Traits\IsFromTargetTrait; use CakeDC\PHPStan\Utility\CakeNameRegistry; use PhpParser\Node\Expr; +use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Identifier; use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; @@ -54,7 +55,7 @@ public function __construct( public function getType(Expr $expr, Scope $scope): ?Type { if ( - !$expr instanceof Expr\MethodCall + !$expr instanceof MethodCall || !$expr->name instanceof Identifier || $expr->name->toString() !== $this->methodName ) { diff --git a/src/Type/ConsoleHelperLoadDynamicReturnTypeExtension.php b/src/Type/ConsoleHelperLoadDynamicReturnTypeExtension.php index 55343d2..adc3f48 100644 --- a/src/Type/ConsoleHelperLoadDynamicReturnTypeExtension.php +++ b/src/Type/ConsoleHelperLoadDynamicReturnTypeExtension.php @@ -13,7 +13,7 @@ namespace CakeDC\PHPStan\Type; -use Cake\Console\ConsoleIo; +use Cake\Console\ConsoleIoInterface; use Cake\Console\Helper; use CakeDC\PHPStan\Traits\BaseCakeRegistryReturnTrait; use CakeDC\PHPStan\Utility\CakeNameRegistry; @@ -50,10 +50,10 @@ class ConsoleHelperLoadDynamicReturnTypeExtension implements DynamicMethodReturn */ public function __construct(?CakeNameRegistry $cakeNameRegistry = null) { - $this->className = ConsoleIo::class; + $this->className = ConsoleIoInterface::class; $this->methodName = 'helper'; $this->defaultClass = Helper::class; - $this->namespaceFormat = '%s\\Command\Helper\\%sHelper'; + $this->namespaceFormat = '%s\\Console\Helper\\%sHelper'; $this->cakeNameRegistry = $cakeNameRegistry ?? CakeNameRegistry::instance(); } diff --git a/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php b/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php index 3f42770..7f50ddd 100644 --- a/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php +++ b/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php @@ -120,7 +120,7 @@ private function getTypeMap(): array /** * @var \PHPStan\Reflection\Php\PhpPropertyReflection $property */ - $property = $reflection->getStaticProperty('_types'); + $property = $reflection->getStaticProperty('types'); /** * @var array $defaultValue */ diff --git a/src/Utility/CakeNameRegistry.php b/src/Utility/CakeNameRegistry.php index 2710a00..a052a03 100644 --- a/src/Utility/CakeNameRegistry.php +++ b/src/Utility/CakeNameRegistry.php @@ -3,6 +3,7 @@ namespace CakeDC\PHPStan\Utility; +use PHPStan\Reflection\ReflectionProvider; use function Cake\Core\pluginSplit; class CakeNameRegistry @@ -10,8 +11,10 @@ class CakeNameRegistry /** * @param string $appNamespace The application's namespace. */ - public function __construct(private readonly string $appNamespace) - { + public function __construct( + private readonly string $appNamespace, + private readonly ?ReflectionProvider $reflectionProvider = null, + ) { } /** @@ -40,7 +43,7 @@ protected static function pluginSplit(string $baseName): array public function getClassName(string $baseName, string|array $namespaceFormat): ?string { if (str_contains($baseName, '\\')) { - return class_exists($baseName) ? $baseName : null; + return $this->hasClass($baseName) ? $baseName : null; } [$plugin, $name] = static::pluginSplit($baseName); @@ -50,7 +53,7 @@ public function getClassName(string $baseName, string|array $namespaceFormat): ? foreach ($prefixes as $prefix) { $namespace = str_replace('/', '\\', $prefix); $className = sprintf($format, $namespace, $name); - if (class_exists($className)) { + if ($this->hasClass($className)) { return $className; } } @@ -59,6 +62,15 @@ public function getClassName(string $baseName, string|array $namespaceFormat): ? return null; } + /** + * @param string $className + * @return bool + */ + private function hasClass(string $className): bool + { + return $this->reflectionProvider?->hasClass($className) ?? class_exists($className); + } + /** * @param string $name * @return string|null diff --git a/src/Visitor/AddAssociationSetClassNameVisitor.php b/src/Visitor/AddAssociationSetClassNameVisitor.php index 4c76d22..8b9f29d 100644 --- a/src/Visitor/AddAssociationSetClassNameVisitor.php +++ b/src/Visitor/AddAssociationSetClassNameVisitor.php @@ -8,6 +8,7 @@ use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Identifier; use PhpParser\NodeVisitorAbstract; class AddAssociationSetClassNameVisitor extends NodeVisitorAbstract @@ -40,7 +41,7 @@ public function beforeTraverse(array $nodes): ?array */ public function enterNode(Node $node): ?Node { - if (!$node instanceof MethodCall || !$node->name instanceof Node\Identifier) { + if (!$node instanceof MethodCall || !$node->name instanceof Identifier) { return null; } if ($this->optionsSet === null && $node->name->name === 'setClassName') { diff --git a/tests/TestCase/Rule/Model/OrmSelectQueryFindMatchOptionsTypesRuleTest.php b/tests/TestCase/Rule/Model/OrmSelectQueryFindMatchOptionsTypesRuleTest.php index 8331638..593cfb1 100644 --- a/tests/TestCase/Rule/Model/OrmSelectQueryFindMatchOptionsTypesRuleTest.php +++ b/tests/TestCase/Rule/Model/OrmSelectQueryFindMatchOptionsTypesRuleTest.php @@ -32,24 +32,24 @@ public function testRule(): void $this->analyse([__DIR__ . '/Fake/FailingOrmFindRuleItemsLogic.php'], [ ['Call to App\Model\Table\NotesTable::find with option "select" (array|Cake\Database\ExpressionInterface|Cake\ORM\Association|Cake\ORM\Table|Closure|float|int|string) does not accept true.', 37], ['Call to App\Model\Table\NotesTable::find with option "fields" (array|Cake\Database\ExpressionInterface|Cake\ORM\Association|Cake\ORM\Table|Closure|float|int|string) does not accept false.', 37], - ['Call to App\Model\Table\NotesTable::find with option "conditions" (array|Cake\Database\ExpressionInterface|Closure|string|null) does not accept stdClass.', 37], - ['Call to App\Model\Table\NotesTable::find with option "where" (array|Cake\Database\ExpressionInterface|Closure|string|null) does not accept true.', 37], + ['Call to App\Model\Table\NotesTable::find with option "conditions" (array|Cake\Database\ExpressionInterface|(Closure(Cake\Database\Expression\QueryExpression, static): (array|Cake\Database\ExpressionInterface|string))|string|null) does not accept stdClass.', 37], + ['Call to App\Model\Table\NotesTable::find with option "where" (array|Cake\Database\ExpressionInterface|(Closure(Cake\Database\Expression\QueryExpression, static): (array|Cake\Database\ExpressionInterface|string))|string|null) does not accept true.', 37], ['Call to App\Model\Table\NotesTable::find with option "order" (array|Cake\Database\ExpressionInterface|Closure|string) does not accept false.', 37], ['Call to App\Model\Table\NotesTable::find with option "orderBy" (array|Cake\Database\ExpressionInterface|Closure|string) does not accept true.', 37], ['Call to App\Model\Table\NotesTable::find with option "limit" (Cake\Database\ExpressionInterface|int|null) does not accept string.', 37], ['Call to App\Model\Table\NotesTable::find with option "offset" (Cake\Database\ExpressionInterface|int|null) does not accept string.', 37], ['Call to App\Model\Table\NotesTable::find with option "group" (array|Cake\Database\ExpressionInterface|string) does not accept false.', 37], ['Call to App\Model\Table\NotesTable::find with option "groupBy" (array|Cake\Database\ExpressionInterface|string) does not accept false.', 37], - ['Call to App\Model\Table\NotesTable::find with option "having" (array|Cake\Database\ExpressionInterface|Closure|string|null) does not accept stdClass.', 37], + ['Call to App\Model\Table\NotesTable::find with option "having" (array|Cake\Database\ExpressionInterface|(Closure(Cake\Database\Expression\QueryExpression, static): (array|Cake\Database\ExpressionInterface|string))|string|null) does not accept stdClass.', 37], ['Call to App\Model\Table\NotesTable::find with option "contain" (array|string) does not accept true.', 37], ['Call to App\Model\Table\NotesTable::find with option "page" (int) does not accept string.', 37], - ['Call to App\Model\Table\NotesTable::find with option "conditions" (array|Cake\Database\ExpressionInterface|Closure|string|null) does not accept false.', 65], + ['Call to App\Model\Table\NotesTable::find with option "conditions" (array|Cake\Database\ExpressionInterface|(Closure(Cake\Database\Expression\QueryExpression, static): (array|Cake\Database\ExpressionInterface|string))|string|null) does not accept false.', 65], ['Call to App\Model\Table\NotesTable::find with option "limit" (Cake\Database\ExpressionInterface|int|null) does not accept stdClass.', 65], ['Call to App\Model\Table\NotesTable::find with option "group" (array|Cake\Database\ExpressionInterface|string) does not accept true.', 65], ['Call to Cake\ORM\Query\SelectQuery::find with option "select" (array|Cake\Database\ExpressionInterface|Cake\ORM\Association|Cake\ORM\Table|Closure|float|int|string) does not accept false.', 77], - ['Call to Cake\ORM\Query\SelectQuery::find with option "conditions" (array|Cake\Database\ExpressionInterface|Closure|string|null) does not accept stdClass.', 77], + ['Call to Cake\ORM\Query\SelectQuery::find with option "conditions" (array|Cake\Database\ExpressionInterface|(Closure(Cake\Database\Expression\QueryExpression, static): (array|Cake\Database\ExpressionInterface|string))|string|null) does not accept stdClass.', 77], ['Call to Cake\ORM\Query\SelectQuery::find with option "offset" (Cake\Database\ExpressionInterface|int|null) does not accept string.', 77], - ['Call to App\Model\Table\UsersTable::find with option "conditions" (array|Cake\Database\ExpressionInterface|Closure|string|null) does not accept false.', 83], + ['Call to App\Model\Table\UsersTable::find with option "conditions" (array|Cake\Database\ExpressionInterface|(Closure(Cake\Database\Expression\QueryExpression, static): (array|Cake\Database\ExpressionInterface|string))|string|null) does not accept false.', 83], ['Call to App\Model\Table\UsersTable::find with option "limit" (Cake\Database\ExpressionInterface|int|null) does not accept stdClass.', 83], ['Call to App\Model\Table\UsersTable::find with option "page" (int) does not accept string.', 83], ['Call to App\Model\Table\NotesTable::find with option "groupField" (array|Closure|string|null) does not accept false.', 95], diff --git a/tests/TestCase/Rule/Model/TableGetMatchOptionsTypesRuleTest.php b/tests/TestCase/Rule/Model/TableGetMatchOptionsTypesRuleTest.php index fba9259..cf15b5c 100644 --- a/tests/TestCase/Rule/Model/TableGetMatchOptionsTypesRuleTest.php +++ b/tests/TestCase/Rule/Model/TableGetMatchOptionsTypesRuleTest.php @@ -32,25 +32,25 @@ public function testRule(): void $this->analyse([__DIR__ . '/Fake/FailingTableGetRuleItemsLogic.php'], [ ['Call to App\Model\Table\NotesTable::get with option "select" (array|Cake\Database\ExpressionInterface|Cake\ORM\Association|Cake\ORM\Table|Closure|float|int|string) does not accept true.', 37], ['Call to App\Model\Table\NotesTable::get with option "fields" (array|Cake\Database\ExpressionInterface|Cake\ORM\Association|Cake\ORM\Table|Closure|float|int|string) does not accept false.', 37], - ['Call to App\Model\Table\NotesTable::get with option "conditions" (array|Cake\Database\ExpressionInterface|Closure|string|null) does not accept stdClass.', 37], - ['Call to App\Model\Table\NotesTable::get with option "where" (array|Cake\Database\ExpressionInterface|Closure|string|null) does not accept true.', 37], + ['Call to App\Model\Table\NotesTable::get with option "conditions" (array|Cake\Database\ExpressionInterface|(Closure(Cake\Database\Expression\QueryExpression, static): (array|Cake\Database\ExpressionInterface|string))|string|null) does not accept stdClass.', 37], + ['Call to App\Model\Table\NotesTable::get with option "where" (array|Cake\Database\ExpressionInterface|(Closure(Cake\Database\Expression\QueryExpression, static): (array|Cake\Database\ExpressionInterface|string))|string|null) does not accept true.', 37], ['Call to App\Model\Table\NotesTable::get with option "order" (array|Cake\Database\ExpressionInterface|Closure|string) does not accept false.', 37], ['Call to App\Model\Table\NotesTable::get with option "orderBy" (array|Cake\Database\ExpressionInterface|Closure|string) does not accept true.', 37], ['Call to App\Model\Table\NotesTable::get with option "limit" (Cake\Database\ExpressionInterface|int|null) does not accept string.', 37], ['Call to App\Model\Table\NotesTable::get with option "offset" (Cake\Database\ExpressionInterface|int|null) does not accept string.', 37], ['Call to App\Model\Table\NotesTable::get with option "group" (array|Cake\Database\ExpressionInterface|string) does not accept false.', 37], ['Call to App\Model\Table\NotesTable::get with option "groupBy" (array|Cake\Database\ExpressionInterface|string) does not accept false.', 37], - ['Call to App\Model\Table\NotesTable::get with option "having" (array|Cake\Database\ExpressionInterface|Closure|string|null) does not accept stdClass.', 37], + ['Call to App\Model\Table\NotesTable::get with option "having" (array|Cake\Database\ExpressionInterface|(Closure(Cake\Database\Expression\QueryExpression, static): (array|Cake\Database\ExpressionInterface|string))|string|null) does not accept stdClass.', 37], ['Call to App\Model\Table\NotesTable::get with option "contain" (array|string) does not accept true.', 37], ['Call to App\Model\Table\NotesTable::get with option "page" (int) does not accept string.', 37], ['Call to App\Model\Table\NotesTable::get with option "order" (array|Cake\Database\ExpressionInterface|Closure|string) does not accept false.', 65], ['Call to App\Model\Table\NotesTable::get with option "limit" (Cake\Database\ExpressionInterface|int|null) does not accept string.', 65], - ['Call to App\Model\Table\NotesTable::get with option "conditions" (array|Cake\Database\ExpressionInterface|Closure|string|null) does not accept stdClass.', 69], + ['Call to App\Model\Table\NotesTable::get with option "conditions" (array|Cake\Database\ExpressionInterface|(Closure(Cake\Database\Expression\QueryExpression, static): (array|Cake\Database\ExpressionInterface|string))|string|null) does not accept stdClass.', 69], ['Call to App\Model\Table\NotesTable::get with option "offset" (Cake\Database\ExpressionInterface|int|null) does not accept string.', 69], ['Call to App\Model\Table\NotesTable::get with option "fields" (array|Cake\Database\ExpressionInterface|Cake\ORM\Association|Cake\ORM\Table|Closure|float|int|string) does not accept false.', 73], ['Call to App\Model\Table\NotesTable::get with option "contain" (array|string) does not accept true.', 73], ['Call to App\Model\Table\UsersTable::get with option "groupBy" (array|Cake\Database\ExpressionInterface|string) does not accept false.', 78], - ['Call to App\Model\Table\UsersTable::get with option "having" (array|Cake\Database\ExpressionInterface|Closure|string|null) does not accept stdClass.', 78], + ['Call to App\Model\Table\UsersTable::get with option "having" (array|Cake\Database\ExpressionInterface|(Closure(Cake\Database\Expression\QueryExpression, static): (array|Cake\Database\ExpressionInterface|string))|string|null) does not accept stdClass.', 78], ]); } diff --git a/tests/TestCase/Type/TypeInferenceTest.php b/tests/TestCase/Type/TypeInferenceTest.php new file mode 100644 index 0000000..9ccc0fb --- /dev/null +++ b/tests/TestCase/Type/TypeInferenceTest.php @@ -0,0 +1,37 @@ +> + */ + public static function provideTypes(): array + { + return self::gatherAssertTypes(__DIR__ . '/../../data/registry-types.php'); + } + + /** + * @return void + */ + public function testFixtureIsNotLoadedAtRuntime(): void + { + static::assertFalse(class_exists('StaticAnalysisFixture\\StaticOnlyTable', false)); + } + + #[DataProvider('provideTypes')] + public function testTypes(string $assertType, string $file, mixed ...$args): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } +} diff --git a/tests/data/StaticOnlyTable.php b/tests/data/StaticOnlyTable.php new file mode 100644 index 0000000..178b14d --- /dev/null +++ b/tests/data/StaticOnlyTable.php @@ -0,0 +1,10 @@ +get('VeryCustomize00009Articles')); + assertType( + 'CakeDC\\MyPlugin\\Model\\Table\\HelloWorld101010ArticlesTable', + $locator->get('CakeDC/MyPlugin.HelloWorld101010Articles') + ); + assertType('Cake\\ORM\\Table', $locator->get($unknownName)); + assertType('Cake\\ORM\\Table', $locator->get('MissingTable')); + assertType(StaticOnlyTable::class, $locator->get(StaticOnlyTable::class)); + assertType( + 'App\\Model\\Table\\VeryCustomize00009ArticlesTable', + $controller->fetchTable('VeryCustomize00009Articles') + ); + assertType( + 'App\\Model\\Table\\VeryCustomize00009ArticlesTable', + $component->fetchTable('VeryCustomize00009Articles') + ); + assertType( + 'App\\Model\\Table\\VeryCustomize00009ArticlesTable', + $command->fetchTable('VeryCustomize00009Articles') + ); + assertType('App\\Model\\Table\\VeryCustomize00009ArticlesTable', $mailer->fetchTable('VeryCustomize00009Articles')); + assertType('App\\Model\\Table\\VeryCustomize00009ArticlesTable', $cell->fetchTable('VeryCustomize00009Articles')); + assertType('Cake\\ORM\\Table', $controller->fetchTable()); + assertType('Cake\\ORM\\Table', $controller->fetchTable(null)); + assertType('Cake\\ORM\\Table', $controller->fetchTable($choose ? 'VeryCustomize00009Articles' : null)); + assertType('Cake\\Controller\\Component\\FlashComponent', $controller->loadComponent('Flash')); + assertType('Cake\\Controller\\Component', $controller->loadComponent($unknownName)); + assertType('Cake\\Console\\Helper\\ProgressHelper', $io->helper('progress')); + assertType('App\\Console\\Helper\\MyHeadingHelper', $interfaceIo->helper('MyHeading')); + assertType('App\\Console\\Helper\\BazBazHelper', $io->helper('BazBaz')); + assertType('Cake\\Console\\Helper', $io->helper($unknownName)); + assertType( + 'App\\Console\\Helper\\BazBazHelper|App\\Console\\Helper\\MyHeadingHelper', + $io->helper($choose ? 'BazBaz' : 'MyHeading') + ); + assertType('Cake\\ORM\\Query\\SelectQuery', $table->findByTitle('sample')); + assertType('Cake\\ORM\\Query\\SelectQuery', (new StaticOnlyTable())->findByTitle('sample')); + assertType('bool', $association->exists(['id' => 1])); +} diff --git a/tests/phpstan.neon b/tests/phpstan.neon new file mode 100644 index 0000000..6d4252f --- /dev/null +++ b/tests/phpstan.neon @@ -0,0 +1,2 @@ +includes: + - ../extension.neon diff --git a/tests/test_app/Command/MyTestExtendedCommand.php b/tests/test_app/Command/MyTestExtendedCommand.php index b4e4ac1..ac5f576 100644 --- a/tests/test_app/Command/MyTestExtendedCommand.php +++ b/tests/test_app/Command/MyTestExtendedCommand.php @@ -13,18 +13,17 @@ namespace App\Command; -use Cake\Console\Arguments; -use Cake\Console\ConsoleIo; - class MyTestExtendedCommand extends MyTestLoadCommand { /** * @inheritDoc */ - public function execute(Arguments $args, ConsoleIo $io) + public function execute(): ?int { $this->fetchTable('VeryCustomize00009Articles')->newSample(); - $io->helper('MyHeading')->headingOne('Sample Text 02'); + $this->io->helper('MyHeading')->headingOne('Sample Text 02'); $this->getMailer('MyTestLoad')->testing(); + + return null; } } diff --git a/tests/test_app/Command/MyTestLoadCommand.php b/tests/test_app/Command/MyTestLoadCommand.php index e94aab1..da197f9 100644 --- a/tests/test_app/Command/MyTestLoadCommand.php +++ b/tests/test_app/Command/MyTestLoadCommand.php @@ -14,8 +14,6 @@ namespace App\Command; use Cake\Command\Command; -use Cake\Console\Arguments; -use Cake\Console\ConsoleIo; use Cake\Mailer\MailerAwareTrait; class MyTestLoadCommand extends Command @@ -25,13 +23,15 @@ class MyTestLoadCommand extends Command /** * @inheritDoc */ - public function execute(Arguments $args, ConsoleIo $io) + public function execute(): ?int { $this->fetchTable('VeryCustomize00009Articles')->newSample(); - $io->helper('progress')->increment(1); - $io->out($io->helper('BazBaz')->foo()); - $io->helper('MyHeading')->headingOne('Sample Text 01'); + $this->io->helper('progress')->increment(1); + $this->io->out($this->io->helper('BazBaz')->foo()); + $this->io->helper('MyHeading')->headingOne('Sample Text 01'); $this->getMailer('MyTestLoad')->testing(); + + return null; } } diff --git a/tests/test_app/Command/Helper/BazBazHelper.php b/tests/test_app/Console/Helper/BazBazHelper.php similarity index 84% rename from tests/test_app/Command/Helper/BazBazHelper.php rename to tests/test_app/Console/Helper/BazBazHelper.php index 671d039..c2fee5a 100644 --- a/tests/test_app/Command/Helper/BazBazHelper.php +++ b/tests/test_app/Console/Helper/BazBazHelper.php @@ -11,21 +11,21 @@ * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -namespace App\Command\Helper; +namespace App\Console\Helper; use Cake\Console\Helper; class BazBazHelper extends Helper { /** - * This method should output content using `$this->_io`. + * This method should output content using `$this->io`. * * @param array $args The arguments for the helper. * @return void */ public function output(array $args): void { - $this->_io->success('Sample heading'); + $this->io->success('Sample heading'); } /** diff --git a/tests/test_app/Command/Helper/MyHeadingHelper.php b/tests/test_app/Console/Helper/MyHeadingHelper.php similarity index 81% rename from tests/test_app/Command/Helper/MyHeadingHelper.php rename to tests/test_app/Console/Helper/MyHeadingHelper.php index 58caca9..8418778 100644 --- a/tests/test_app/Command/Helper/MyHeadingHelper.php +++ b/tests/test_app/Console/Helper/MyHeadingHelper.php @@ -11,21 +11,21 @@ * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ -namespace App\Command\Helper; +namespace App\Console\Helper; use Cake\Console\Helper; class MyHeadingHelper extends Helper { /** - * This method should output content using `$this->_io`. + * This method should output content using `$this->io`. * * @param array $args The arguments for the helper. * @return void */ public function output(array $args): void { - $this->_io->success('Sample heading'); + $this->io->success('Sample heading'); } /** @@ -36,6 +36,6 @@ public function output(array $args): void */ public function headingOne(string $text): void { - $this->_io->out('# ' . $text . ' #'); + $this->io->out('# ' . $text . ' #'); } } diff --git a/tests/test_app/Controller/Component/TableAwareComponent.php b/tests/test_app/Controller/Component/TableAwareComponent.php new file mode 100644 index 0000000..8543749 --- /dev/null +++ b/tests/test_app/Controller/Component/TableAwareComponent.php @@ -0,0 +1,12 @@ + $year, ]; if ($fun === true) { - $where[] = $query->newExpr()->in( + $where[] = $query->expr()->in( 'type', ['funny_stuff', 'funny_songs', 'funny_messages'], ); diff --git a/tests/test_app/View/Cell/MyTestLoadCell.php b/tests/test_app/View/Cell/MyTestLoadCell.php index 123f169..f9aac01 100644 --- a/tests/test_app/View/Cell/MyTestLoadCell.php +++ b/tests/test_app/View/Cell/MyTestLoadCell.php @@ -20,7 +20,7 @@ class MyTestLoadCell extends Cell protected ?string $defaultTable = 'Users'; /** - * Test for TableLocatorDynamicReturnTypeExtension with loadModel + * Test for TableLocatorDynamicReturnTypeExtension with fetchTable * * @return void */