From e010a8b57a2972a6813aa25f7d9532b1b5735dc3 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Tue, 18 Jul 2023 08:03:59 +0200 Subject: [PATCH] Introduce validation of direct creation of configurable classes --- rules.neon | 1 + src/Rule/CreateConfigurableObjectRule.php | 89 +++++++++++++++++++ .../Rule/CreateConfigurableObjectRuleTest.php | 33 +++++++ .../create_configurable_object_invalid.php | 17 ++++ .../create_configurable_object_valid.php | 26 ++++++ tests/Yii/MyComponent.php | 2 +- 6 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 src/Rule/CreateConfigurableObjectRule.php create mode 100644 tests/Rule/CreateConfigurableObjectRuleTest.php create mode 100644 tests/Rule/_data/create_configurable_object_invalid.php create mode 100644 tests/Rule/_data/create_configurable_object_valid.php diff --git a/rules.neon b/rules.neon index 7a7d63f..859fdc1 100644 --- a/rules.neon +++ b/rules.neon @@ -1,2 +1,3 @@ rules: + - ErickSkrauch\PHPStan\Yii2\Rule\CreateConfigurableObjectRule - ErickSkrauch\PHPStan\Yii2\Rule\CreateObjectRule diff --git a/src/Rule/CreateConfigurableObjectRule.php b/src/Rule/CreateConfigurableObjectRule.php new file mode 100644 index 0000000..9e08168 --- /dev/null +++ b/src/Rule/CreateConfigurableObjectRule.php @@ -0,0 +1,89 @@ + + */ +final class CreateConfigurableObjectRule implements Rule { + + private ReflectionProvider $reflectionProvider; + + private YiiConfigHelper $configHelper; + + public function __construct(ReflectionProvider $reflectionProvider, YiiConfigHelper $configHelper) { + $this->reflectionProvider = $reflectionProvider; + $this->configHelper = $configHelper; + } + + public function getNodeType(): string { + return New_::class; + } + + /** + * @param New_ $node + */ + public function processNode(Node $node, Scope $scope): array { + $calledOn = $node->class; + if (!$calledOn instanceof Node\Name) { + return []; + } + + $className = $calledOn->toString(); + + // Invalid call, leave it for another rules + if (!$this->reflectionProvider->hasClass($className)) { + return []; + } + + $class = $this->reflectionProvider->getClass($className); + // This rule intended for use only with Configurable interface + if (!$class->is(Configurable::class)) { + return []; + } + + $constructorParams = ParametersAcceptorSelector::selectSingle($class->getConstructor()->getVariants())->getParameters(); + $lastArgName = $constructorParams[array_key_last($constructorParams)]->getName(); + + $args = $node->args; + foreach ($args as $arg) { + // Try to find config by named argument + if ($arg instanceof Node\Arg && $arg->name !== null && $arg->name->name === $lastArgName) { + $configArg = $arg; + break; + } + } + + // Attempt to find by named arg failed, try to find it by index + if (!isset($configArg) && isset($args[count($constructorParams) - 1])) { + $configArg = $args[count($constructorParams) - 1]; + // At this moment I don't know what to do with variadic arguments + if (!$configArg instanceof Node\Arg) { + return []; + } + } + + // Config arg wasn't specified, so nothing to validate + if (!isset($configArg)) { + return []; + } + + $configArgType = $scope->getType($configArg->value); + $errors = []; + foreach ($configArgType->getConstantArrays() as $constantArray) { + $errors = array_merge($errors, $this->configHelper->validateArray($class, $constantArray, $scope)); + } + + return $errors; + } + +} diff --git a/tests/Rule/CreateConfigurableObjectRuleTest.php b/tests/Rule/CreateConfigurableObjectRuleTest.php new file mode 100644 index 0000000..51b78bd --- /dev/null +++ b/tests/Rule/CreateConfigurableObjectRuleTest.php @@ -0,0 +1,33 @@ + + */ +final class CreateConfigurableObjectRuleTest extends RuleTestCase { + use ConfigTrait; + + public function testRule(): void { + $this->analyse([__DIR__ . '/_data/create_configurable_object_valid.php'], []); + $this->analyse([__DIR__ . '/_data/create_configurable_object_invalid.php'], [ + ['Property ErickSkrauch\PHPStan\Yii2\Tests\Yii\MyComponent::$privateStringProp (string) does not accept int.', 10], + ['Property ErickSkrauch\PHPStan\Yii2\Tests\Yii\MyComponent::$privateStringProp (string) does not accept int.', 15], + ]); + } + + protected function getRule(): Rule { + return new CreateConfigurableObjectRule( + self::createReflectionProvider(), + self::getContainer()->getByType(YiiConfigHelper::class), + ); + } + +} diff --git a/tests/Rule/_data/create_configurable_object_invalid.php b/tests/Rule/_data/create_configurable_object_invalid.php new file mode 100644 index 0000000..bd2be55 --- /dev/null +++ b/tests/Rule/_data/create_configurable_object_invalid.php @@ -0,0 +1,17 @@ + 123, +]); + +// Param passed as a named arg +new MyComponent('string', config: [ + 'privateStringProp' => 123, +]); diff --git a/tests/Rule/_data/create_configurable_object_valid.php b/tests/Rule/_data/create_configurable_object_valid.php new file mode 100644 index 0000000..1147693 --- /dev/null +++ b/tests/Rule/_data/create_configurable_object_valid.php @@ -0,0 +1,26 @@ + 'string', +]); + +// Param passed as a named arg +new MyComponent('string', config: [ + 'privateStringProp' => 'string', +]); + +// Some real world usage +new \yii\widgets\DetailView([ + 'model' => new \ErickSkrauch\PHPStan\Yii2\Tests\Yii\Article(), + 'attributes' => [ + 'id', + 'text', + ], +]); diff --git a/tests/Yii/MyComponent.php b/tests/Yii/MyComponent.php index 580583d..d252498 100644 --- a/tests/Yii/MyComponent.php +++ b/tests/Yii/MyComponent.php @@ -24,7 +24,7 @@ final class MyComponent extends Component { private string $_privateStringProp = ''; // @phpstan-ignore-next-line ignore unused arguments errors and missing $config type - public function __construct(string $stringArg, int $intArg, array $config = []) { + public function __construct(string $stringArg, int $intArg = 0, array $config = []) { parent::__construct($config); }