diff --git a/LICENSE b/LICENSE index d36a04b..c41d93e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,25 @@ -MIT License +The MIT License +--------------- -Copyright (c) 2022 Andrew Riddlestone +Copyright (c) 2023 Oleg Zhulnev (https://github.com/sidz) -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 249667f..9eba9c5 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ PHPStan extensions to help test CakePHP 2 projects with PHPStan Installation is best done through composer: ```shell -composer require --dev ariddlestone/phpstan-cakephp2 +composer require --dev sidz/phpstan-cakephp2 ``` You will need to make sure the extension is included in your phpstan config: ```yaml # phpstan.neon includes: - - vendor/ariddlestone/phpstan-cakephp2/extension.neon + - vendor/sidz/phpstan-cakephp2/extension.neon ``` If you have behavior classes in odd locations (perhaps in a vendor directory) you will need to add those locations to diff --git a/composer.json b/composer.json index 1d3c7b1..c472670 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,12 @@ { - "name": "ariddlestone/phpstan-cakephp2", + "name": "sidz/phpstan-cakephp2", "description": "An extension to help test CakePHP 2 projects with PHPStan", "type": "library", "license": "MIT", "authors": [ { - "name": "Andrew Riddlestone", - "email": "andrew.riddlestone@gmail.com" + "name": "Oleg Zhulnev", + "email": "plbsid@gmail.com" } ], "require": { @@ -21,12 +21,12 @@ }, "autoload": { "psr-4": { - "ARiddlestone\\PHPStanCakePHP2\\": "src/" + "PHPStanCakePHP2\\": "src/" } }, "autoload-dev": { "psr-4": { - "ARiddlestone\\PHPStanCakePHP2\\Test\\": "tests/" + "PHPStanCakePHP2\\Test\\": "tests/" }, "classmap": [ "tests/Feature/classes", @@ -42,7 +42,6 @@ ] }, "scripts": { - "extract-phpstan": "phar extract -f vendor/phpstan/phpstan/phpstan.phar phpstan", "phpinsights": "phpinsights analyse --no-interaction --ansi --config-path=phpinsights.php --summary src", "phpinsights-github": "phpinsights analyse --no-interaction --ansi --config-path=phpinsights.php --format=github-action src", "phpstan": "phpstan", diff --git a/extension.neon b/extension.neon index b1cc782..1b1e7ee 100644 --- a/extension.neon +++ b/extension.neon @@ -9,29 +9,31 @@ parameters: schemaPaths: - app/Config/Schema/*.php stubFiles: - - stubs/Utility.php + - stubs/Model/Model.stub + - stubs/Routing/Router.stub + - stubs/Utility/ClassRegistry.stub services: - - class: ARiddlestone\PHPStanCakePHP2\ClassComponentsExtension + - class: PHPStanCakePHP2\ClassComponentsExtension tags: - phpstan.broker.propertiesClassReflectionExtension - - class: ARiddlestone\PHPStanCakePHP2\ClassModelsExtension + - class: PHPStanCakePHP2\ClassModelsExtension tags: - phpstan.broker.propertiesClassReflectionExtension - - class: ARiddlestone\PHPStanCakePHP2\ClassRegistryInitExtension + - class: PHPStanCakePHP2\ClassRegistryInitExtension tags: - phpstan.broker.dynamicStaticMethodReturnTypeExtension - - class: ARiddlestone\PHPStanCakePHP2\ClassTasksExtension + - class: PHPStanCakePHP2\ClassTasksExtension tags: - phpstan.broker.propertiesClassReflectionExtension - - class: ARiddlestone\PHPStanCakePHP2\ModelBehaviorsExtension + - class: PHPStanCakePHP2\ModelBehaviorsExtension arguments: behaviorPaths: %ModelBehaviorsExtension.behaviorPaths% tags: - phpstan.broker.methodsClassReflectionExtension - - class: ARiddlestone\PHPStanCakePHP2\Service\SchemaService + - class: PHPStanCakePHP2\Service\SchemaService arguments: schemaPaths: %SchemaService.schemaPaths% - - class: ARiddlestone\PHPStanCakePHP2\LoadComponentOnFlyMethodReturnTypeExtension + - class: PHPStanCakePHP2\LoadComponentOnFlyMethodReturnTypeExtension tags: - phpstan.broker.dynamicMethodReturnTypeExtension parametersSchema: diff --git a/phpunit.xml b/phpunit.xml index 9993376..9b8d058 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,6 @@ reflectionProvider = $reflectionProvider; + } + + public function hasProperty(ClassReflection $classReflection, string $propertyName): bool + { + if (!array_filter($this->getContainingClassNames(), [$classReflection, 'is'])) { + return false; + } + + $isDefinedInComponentsProperty = (bool) array_filter( + $this->getDefinedComponentsAsList($classReflection), + static fn (string $componentName): bool => $componentName === $propertyName + ); + + if (!$isDefinedInComponentsProperty) { + return false; + } + + $propertyClassName = $this->getClassNameFromPropertyName($propertyName); + + return $this->reflectionProvider->hasClass($propertyClassName) + && $this->reflectionProvider->getClass($propertyClassName) + ->is('Component'); + } + + public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection + { + return new PublicReadOnlyPropertyReflection( + $this->getClassNameFromPropertyName($propertyName), + $classReflection + ); } /** * @return array */ - protected function getContainingClassNames(): array + private function getContainingClassNames(): array { return [ 'Controller', @@ -22,9 +63,44 @@ protected function getContainingClassNames(): array ]; } - protected function getClassNameFromPropertyName( - string $propertyName - ): string { - return $propertyName . 'Component'; + private function getClassNameFromPropertyName(string $propertyName): string + { + return str_contains($propertyName, 'Component') ? $propertyName : $propertyName . 'Component'; + } + + /** + * @return list + */ + private function getDefinedComponentsAsList(ClassReflection $classReflection): array + { + $definedComponents = []; + + foreach (array_merge([$classReflection], $classReflection->getParents()) as $class) { + if (!$class->hasProperty('components')) { + continue; + } + + $defaultValue = $class->getNativeProperty('components') + ->getNativeReflection() + ->getDefaultValueExpression(); + + if (!$defaultValue instanceof Array_) { + continue; + } + + foreach ($defaultValue->items as $item) { + if ($item->value instanceof String_) { + $definedComponents[] = $item->value->value; + + continue; + } + + if ($item->value instanceof ClassConstFetch && $item->value->class instanceof FullyQualified) { + $definedComponents[] = $item->value->class->toString(); + } + } + } + + return $definedComponents; } } diff --git a/src/ClassModelsExtension.php b/src/ClassModelsExtension.php index b7873b0..e3aec68 100644 --- a/src/ClassModelsExtension.php +++ b/src/ClassModelsExtension.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ARiddlestone\PHPStanCakePHP2; +namespace PHPStanCakePHP2; /** * Adds {@link Model}s as properties to {@link Shell}s. diff --git a/src/ClassPropertiesExtension.php b/src/ClassPropertiesExtension.php index 82a5eb0..3fda068 100644 --- a/src/ClassPropertiesExtension.php +++ b/src/ClassPropertiesExtension.php @@ -2,15 +2,14 @@ declare(strict_types=1); -namespace ARiddlestone\PHPStanCakePHP2; +namespace PHPStanCakePHP2; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\ReflectionProvider; -abstract class ClassPropertiesExtension implements - PropertiesClassReflectionExtension +abstract class ClassPropertiesExtension implements PropertiesClassReflectionExtension { private ReflectionProvider $reflectionProvider; diff --git a/src/ClassReflectionFinder.php b/src/ClassReflectionFinder.php index a4be4ef..cd8f679 100644 --- a/src/ClassReflectionFinder.php +++ b/src/ClassReflectionFinder.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ARiddlestone\PHPStanCakePHP2; +namespace PHPStanCakePHP2; use Exception; use PHPStan\Reflection\ClassReflection; diff --git a/src/ClassRegistryInitExtension.php b/src/ClassRegistryInitExtension.php index baf120d..97cbe8e 100644 --- a/src/ClassRegistryInitExtension.php +++ b/src/ClassRegistryInitExtension.php @@ -1,8 +1,16 @@ getArgs()[0]->value; - $evaluator = new ConstExprEvaluator(); - $arg1 = $evaluator->evaluateSilently($arg1); - if (! is_string($arg1)) { + $argumentType = $scope->getType($methodCall->getArgs()[0]->value); + + if (!$argumentType instanceof ConstantStringType) { return $this->getDefaultType(); } - if ($this->reflectionProvider->hasClass($arg1)) { - return new ObjectType($arg1); + + $value = $argumentType->getValue(); + + if ($this->reflectionProvider->hasClass($value)) { + return new ObjectType($value); } - if ($this->schemaService->hasTable(Inflector::tableize($arg1))) { + + if ($this->schemaService->hasTable(Inflector::tableize($value))) { return new ObjectType('Model'); } + return $this->getDefaultType(); } @@ -61,7 +73,7 @@ private function getDefaultType(): Type { return new UnionType([ new BooleanType(), - new ObjectWithoutClassType() + new ObjectWithoutClassType(), ]); } } diff --git a/src/ClassTasksExtension.php b/src/ClassTasksExtension.php index cbeee89..2d64f9d 100644 --- a/src/ClassTasksExtension.php +++ b/src/ClassTasksExtension.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ARiddlestone\PHPStanCakePHP2; +namespace PHPStanCakePHP2; /** * Adds {@link Model}s as properties to {@link Shell}s. diff --git a/src/LoadComponentOnFlyMethodReturnTypeExtension.php b/src/LoadComponentOnFlyMethodReturnTypeExtension.php index 060e878..af84ea5 100644 --- a/src/LoadComponentOnFlyMethodReturnTypeExtension.php +++ b/src/LoadComponentOnFlyMethodReturnTypeExtension.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ARiddlestone\PHPStanCakePHP2; +namespace PHPStanCakePHP2; use Component; use PhpParser\Node\Expr\MethodCall; diff --git a/src/ModelBehaviorMethodExtractor.php b/src/ModelBehaviorMethodExtractor.php index 55849df..35e07ce 100644 --- a/src/ModelBehaviorMethodExtractor.php +++ b/src/ModelBehaviorMethodExtractor.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ARiddlestone\PHPStanCakePHP2; +namespace PHPStanCakePHP2; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; use PHPStan\Reflection\ClassReflection; @@ -14,10 +14,7 @@ final class ModelBehaviorMethodExtractor { - /** - * @var ClassReflection - */ - private $classReflection; + private ClassReflection $classReflection; public function __construct(ClassReflection $classReflection) { diff --git a/src/ModelBehaviorMethodWrapper.php b/src/ModelBehaviorMethodWrapper.php index f347536..dbc0634 100644 --- a/src/ModelBehaviorMethodWrapper.php +++ b/src/ModelBehaviorMethodWrapper.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ARiddlestone\PHPStanCakePHP2; +namespace PHPStanCakePHP2; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; @@ -18,10 +18,7 @@ */ final class ModelBehaviorMethodWrapper implements MethodReflection { - /** - * @var MethodReflection - */ - private $wrappedMethod; + private MethodReflection $wrappedMethod; public function __construct(MethodReflection $wrappedMethod) { diff --git a/src/ModelBehaviorsExtension.php b/src/ModelBehaviorsExtension.php index f025d52..6350dfc 100644 --- a/src/ModelBehaviorsExtension.php +++ b/src/ModelBehaviorsExtension.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ARiddlestone\PHPStanCakePHP2; +namespace PHPStanCakePHP2; use Exception; use PHPStan\Reflection\ClassReflection; @@ -15,20 +15,17 @@ */ final class ModelBehaviorsExtension implements MethodsClassReflectionExtension { - /** - * @var ReflectionProvider - */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; /** * @var array */ - private $behaviorPaths; + private array $behaviorPaths; /** * @var array|null */ - private $behaviorMethods = null; + private ?array $behaviorMethods = null; /** * @param array $behaviorPaths @@ -54,7 +51,8 @@ public function hasMethod( array_map( [$this, 'getMethodReflectionName'], $this->getBehaviorMethods() - ) + ), + true ); } diff --git a/src/PublicReadOnlyPropertyReflection.php b/src/PublicReadOnlyPropertyReflection.php index a912fa6..a0818ae 100644 --- a/src/PublicReadOnlyPropertyReflection.php +++ b/src/PublicReadOnlyPropertyReflection.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ARiddlestone\PHPStanCakePHP2; +namespace PHPStanCakePHP2; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\PropertyReflection; @@ -12,15 +12,9 @@ final class PublicReadOnlyPropertyReflection implements PropertyReflection { - /** - * @var string - */ - private $name; - - /** - * @var ClassReflection - */ - private $declaringClass; + private string $name; + + private ClassReflection $declaringClass; public function __construct(string $name, ClassReflection $declaringClass) { diff --git a/src/Service/SchemaService.php b/src/Service/SchemaService.php index f9890aa..f296807 100644 --- a/src/Service/SchemaService.php +++ b/src/Service/SchemaService.php @@ -1,8 +1,10 @@ $query + * + * @return ($type is 'count' ? int : (array|int|null)) + */ + public function find($type = 'first', $query = array()) {} +} diff --git a/stubs/Routing/Router.stub b/stubs/Routing/Router.stub new file mode 100644 index 0000000..36250c0 --- /dev/null +++ b/stubs/Routing/Router.stub @@ -0,0 +1,9 @@ +gatherAssertTypes(__DIR__ . '/data/existing_controller_model.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/existing_controller_component.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/invalid_controller_property.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/existing_controller_component_with_same_method_name_as_model.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/existing_controller_component_from_parent_controller.php'); } /** diff --git a/tests/Feature/LoadComponentOnFlyMethodReturnTypeExtensionTest.php b/tests/Feature/LoadComponentOnFlyMethodReturnTypeExtensionTest.php index 78a5214..299aa47 100644 --- a/tests/Feature/LoadComponentOnFlyMethodReturnTypeExtensionTest.php +++ b/tests/Feature/LoadComponentOnFlyMethodReturnTypeExtensionTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ARiddlestone\PHPStanCakePHP2\Test\Feature; +namespace PHPStanCakePHP2\Test\Feature; use PHPStan\Testing\TypeInferenceTestCase; diff --git a/tests/Feature/ModelExtensionsTest.php b/tests/Feature/ModelExtensionsTest.php index 60da68e..50c4a4f 100644 --- a/tests/Feature/ModelExtensionsTest.php +++ b/tests/Feature/ModelExtensionsTest.php @@ -1,6 +1,8 @@ + */ + public $components = ['Basic']; +} diff --git a/tests/Feature/classes/Controller/BasicController.php b/tests/Feature/classes/Controller/BasicController.php index 62c9338..c0318a0 100644 --- a/tests/Feature/classes/Controller/BasicController.php +++ b/tests/Feature/classes/Controller/BasicController.php @@ -1,3 +1,9 @@ + */ + public $components = ['Basic']; +} diff --git a/tests/Feature/classes/Controller/Component/BasicComponent.php b/tests/Feature/classes/Controller/Component/BasicComponent.php index 5d66fa7..64a5018 100644 --- a/tests/Feature/classes/Controller/Component/BasicComponent.php +++ b/tests/Feature/classes/Controller/Component/BasicComponent.php @@ -1,3 +1,9 @@ + */ + public $components = ['Second']; +} diff --git a/tests/Feature/classes/Controller/Component/SameAsModelComponent.php b/tests/Feature/classes/Controller/Component/SameAsModelComponent.php new file mode 100644 index 0000000..3eb6bde --- /dev/null +++ b/tests/Feature/classes/Controller/Component/SameAsModelComponent.php @@ -0,0 +1,9 @@ + + */ + public $components = [ + 'SameAsModel', + BasicComponent::class, + ]; +} diff --git a/tests/Feature/classes/Model/SameAsModel.php b/tests/Feature/classes/Model/SameAsModel.php new file mode 100644 index 0000000..8f377c8 --- /dev/null +++ b/tests/Feature/classes/Model/SameAsModel.php @@ -0,0 +1,9 @@ +Basic; + +assertType('BasicComponent', $component); diff --git a/tests/Feature/data/existing_controller_component_with_same_method_name_as_model.php b/tests/Feature/data/existing_controller_component_with_same_method_name_as_model.php new file mode 100644 index 0000000..d5c8ec3 --- /dev/null +++ b/tests/Feature/data/existing_controller_component_with_same_method_name_as_model.php @@ -0,0 +1,15 @@ +SameAsModel->sameMethod(); + +assertType('int', $component); + +/** @var SameAsModelController $controller */ +$component = $controller->BasicComponent; + +assertType('BasicComponent', $component);