diff --git a/src/ClassComponentsExtension.php b/src/ClassComponentsExtension.php index 689d227..f5b8751 100644 --- a/src/ClassComponentsExtension.php +++ b/src/ClassComponentsExtension.php @@ -4,17 +4,56 @@ namespace PHPStanCakePHP2; -final class ClassComponentsExtension extends ClassPropertiesExtension +use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Scalar\String_; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\PropertiesClassReflectionExtension; +use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ReflectionProvider; + +final class ClassComponentsExtension implements PropertiesClassReflectionExtension { - protected function getPropertyParentClassName(): string + private ReflectionProvider $reflectionProvider; + + public function __construct(ReflectionProvider $reflectionProvider) + { + $this->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 'Component'; + return new PublicReadOnlyPropertyReflection( + $this->getClassNameFromPropertyName($propertyName), + $classReflection + ); } /** * @return array */ - protected function getContainingClassNames(): array + private function getContainingClassNames(): array { return [ 'Controller', @@ -22,9 +61,38 @@ protected function getContainingClassNames(): array ]; } - protected function getClassNameFromPropertyName( - string $propertyName - ): string { + private function getClassNameFromPropertyName(string $propertyName): string + { return $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 !== null && $item->value instanceof String_) { + $definedComponents[] = $item->value->value; + } + } + } + + return $definedComponents; + } } diff --git a/tests/Feature/ControllerExtensionsTest.php b/tests/Feature/ControllerExtensionsTest.php index 7be96c5..5207719 100644 --- a/tests/Feature/ControllerExtensionsTest.php +++ b/tests/Feature/ControllerExtensionsTest.php @@ -16,6 +16,8 @@ public function dataFileAsserts(): iterable yield from $this->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/classes/Controller/BaseController.php b/tests/Feature/classes/Controller/BaseController.php new file mode 100644 index 0000000..692ec43 --- /dev/null +++ b/tests/Feature/classes/Controller/BaseController.php @@ -0,0 +1,11 @@ + + */ + 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']; +} 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..31d64f8 --- /dev/null +++ b/tests/Feature/data/existing_controller_component_with_same_method_name_as_model.php @@ -0,0 +1,10 @@ +SameAsModel->sameMethod(); + +assertType('int', $component);