Skip to content

Commit

Permalink
Introduce validation of direct creation of configurable classes
Browse files Browse the repository at this point in the history
  • Loading branch information
erickskrauch committed Jul 18, 2023
1 parent d3b9fd4 commit e010a8b
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 1 deletion.
1 change: 1 addition & 0 deletions rules.neon
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
rules:
- ErickSkrauch\PHPStan\Yii2\Rule\CreateConfigurableObjectRule
- ErickSkrauch\PHPStan\Yii2\Rule\CreateObjectRule
89 changes: 89 additions & 0 deletions src/Rule/CreateConfigurableObjectRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);

namespace ErickSkrauch\PHPStan\Yii2\Rule;

use PhpParser\Node;
use PhpParser\Node\Expr\New_;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;
use yii\base\Configurable;

/**
* @implements Rule<New_>
*/
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;
}

}
33 changes: 33 additions & 0 deletions tests/Rule/CreateConfigurableObjectRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);

namespace ErickSkrauch\PHPStan\Yii2\Tests\Rule;

use ErickSkrauch\PHPStan\Yii2\Rule\CreateConfigurableObjectRule;
use ErickSkrauch\PHPStan\Yii2\Rule\YiiConfigHelper;
use ErickSkrauch\PHPStan\Yii2\Tests\ConfigTrait;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<CreateConfigurableObjectRule>
*/
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),
);
}

}
17 changes: 17 additions & 0 deletions tests/Rule/_data/create_configurable_object_invalid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);

use ErickSkrauch\PHPStan\Yii2\Tests\Yii\MyComponent;

// Config param wasn't used
new MyComponent('string', 123);

// Param passed as a config arg
new MyComponent('string', 123, [
'privateStringProp' => 123,
]);

// Param passed as a named arg
new MyComponent('string', config: [
'privateStringProp' => 123,
]);
26 changes: 26 additions & 0 deletions tests/Rule/_data/create_configurable_object_valid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);

use ErickSkrauch\PHPStan\Yii2\Tests\Yii\MyComponent;

// Config param wasn't used
new MyComponent('string', 123);

// Param passed as a config arg
new MyComponent('string', 123, [
'privateStringProp' => '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',
],
]);
2 changes: 1 addition & 1 deletion tests/Yii/MyComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down

0 comments on commit e010a8b

Please sign in to comment.