Skip to content

Commit

Permalink
Merge pull request #38 from CakeDC/feature/new-rules-2410
Browse files Browse the repository at this point in the history
Feature/new rules 2410
  • Loading branch information
steinkel authored Oct 21, 2024
2 parents aa2e842 + 062a6d2 commit e7bb4a4
Show file tree
Hide file tree
Showing 20 changed files with 610 additions and 45 deletions.
4 changes: 2 additions & 2 deletions .semver
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
:major: 3
:minor: 1
:patch: 2
:minor: 2
:patch: 0
:special: ''
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ Features included:
1. Provide correct return type for `Cake\ORM\Table::saveManyOrFail` based on the first argument passed
1. Provide correct return type for `Cake\ORM\Table::deleteMany` based on the first argument passed
1. Provide correct return type for `Cake\ORM\Table::deleteManyOrFail` based on the first argument passed

1. Provide correct return type for `Cake\ORM\Locator\LocatorAwareTrait::fetchTable` based on the first argument passed
1. Provide correct return type for `Cake\Mailer\MailerAwareTrait::getMailer` based on the first argument passed

<details>
<summary>Examples:</summary>
Expand Down Expand Up @@ -130,14 +131,31 @@ This rule check if association options are valid option types based on what each
Table::hasMany, Table::belongsToMany, Table::hasOne and AssociationCollection::load.

### AddBehaviorExistsClassRule
This rule check if the target behavior has a valid table class when calling to Table::addBehavior and BehaviorRegistry::load.
This rule check if the target behavior has a valid class when calling to Table::addBehavior and BehaviorRegistry::load.

### DisallowEntityArrayAccessRule
This rule disallow array access to entity in favor of object notation, is easier to detect a wrong property and to refactor code.

### GetMailerExistsClassRule
This rule check if the target mailer is a valid class when calling to Cake\Mailer\MailerAwareTrait::getMailer.

### LoadComponentExistsClassRule
This rule check if the target component has a valid class when calling to Controller::loadComponent and ComponentRegistry::load.

### OrmSelectQueryFindMatchOptionsTypesRule
This rule check if the options (args) passed to Table::find and SelectQuery are valid find options types.

### TableGetMatchOptionsTypesRule
This rule check if the options (args) passed to Table::get are valid find options types.

To enable this rule update your phpstan.neon with:

```
parameters:
cakeDC:
disallowEntityArrayAccessRule: true
```

### How to disable a rule
Each rule has a parameter in cakeDC 'namespace' to enable or disable, it is the same name of the
rule with first letter in lowercase.
Expand Down
2 changes: 2 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ parameters:
level: max
checkGenericClassInNonGenericObjectType: false
treatPhpDocTypesAsCertain: false
cakeDC:
disallowEntityArrayAccessRule: true
19 changes: 18 additions & 1 deletion rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,36 @@ parameters:
addBehaviorExistsClassRule: true
tableGetMatchOptionsTypesRule: true
ormSelectQueryFindMatchOptionsTypesRule: true

disallowEntityArrayAccessRule: false
getMailerExistsClassRule: true
loadComponentExistsClassRule: true
parametersSchema:
cakeDC: structure([
addAssociationExistsTableClassRule: anyOf(bool(), arrayOf(bool()))
addAssociationMatchOptionsTypesRule: anyOf(bool(), arrayOf(bool()))
addBehaviorExistsClassRule: anyOf(bool(), arrayOf(bool()))
tableGetMatchOptionsTypesRule: anyOf(bool(), arrayOf(bool()))
ormSelectQueryFindMatchOptionsTypesRule: anyOf(bool(), arrayOf(bool()))
disallowEntityArrayAccessRule: anyOf(bool(), arrayOf(bool()))
getMailerExistsClassRule: anyOf(bool(), arrayOf(bool()))
loadComponentExistsClassRule: anyOf(bool(), arrayOf(bool()))
])

conditionalTags:
CakeDC\PHPStan\Visitor\AddAssociationSetClassNameVisitor:
phpstan.parser.richParserNodeVisitor: %cakeDC.addAssociationExistsTableClassRule%
CakeDC\PHPStan\Rule\Controller\LoadComponentExistsClassRule:
phpstan.rules.rule: %cakeDC.loadComponentExistsClassRule%
CakeDC\PHPStan\Rule\Model\AddAssociationExistsTableClassRule:
phpstan.rules.rule: %cakeDC.addAssociationExistsTableClassRule%
CakeDC\PHPStan\Rule\Model\AddAssociationMatchOptionsTypesRule:
phpstan.rules.rule: %cakeDC.addAssociationMatchOptionsTypesRule%
CakeDC\PHPStan\Rule\Model\AddBehaviorExistsClassRule:
phpstan.rules.rule: %cakeDC.addBehaviorExistsClassRule%
CakeDC\PHPStan\Rule\Model\DisallowEntityArrayAccessRule:
phpstan.rules.rule: %cakeDC.disallowEntityArrayAccessRule%
CakeDC\PHPStan\Rule\Mailer\GetMailerExistsClassRule:
phpstan.rules.rule: %cakeDC.getMailerExistsClassRule%
CakeDC\PHPStan\Rule\Model\TableGetMatchOptionsTypesRule:
phpstan.rules.rule: %cakeDC.tableGetMatchOptionsTypesRule%
CakeDC\PHPStan\Rule\Model\OrmSelectQueryFindMatchOptionsTypesRule:
Expand All @@ -32,12 +43,18 @@ conditionalTags:
services:
-
class: CakeDC\PHPStan\Visitor\AddAssociationSetClassNameVisitor
-
class: CakeDC\PHPStan\Rule\Controller\LoadComponentExistsClassRule
-
class: CakeDC\PHPStan\Rule\Model\AddAssociationExistsTableClassRule
-
class: CakeDC\PHPStan\Rule\Model\AddAssociationMatchOptionsTypesRule
-
class: CakeDC\PHPStan\Rule\Model\AddBehaviorExistsClassRule
-
class: CakeDC\PHPStan\Rule\Model\DisallowEntityArrayAccessRule
-
class: CakeDC\PHPStan\Rule\Mailer\GetMailerExistsClassRule
-
class: CakeDC\PHPStan\Rule\Model\TableGetMatchOptionsTypesRule
-
Expand Down
71 changes: 71 additions & 0 deletions src/Rule/Controller/LoadComponentExistsClassRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);

/**
* Copyright 2024, Cake Development Corporation (https://www.cakedc.com)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2024, Cake Development Corporation (https://www.cakedc.com)
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/

namespace CakeDC\PHPStan\Rule\Controller;

use Cake\Controller\ComponentRegistry;
use CakeDC\PHPStan\Rule\LoadObjectExistsCakeClassRule;
use CakeDC\PHPStan\Utility\CakeNameRegistry;

class LoadComponentExistsClassRule extends LoadObjectExistsCakeClassRule
{
/**
* @var string
*/
protected string $identifier = 'cake.loadComponent.existClass';

/**
* @var array<string>
*/
protected array $sourceMethods = [
'loadComponent',
];

/**
* @var array<string>
*/
protected array $componentRegistryMethods = [
'load',
];

/**
* @inheritDoc
*/
protected function getTargetClassName(string $name): ?string
{
return CakeNameRegistry::getComponentClassName($name);
}

/**
* @inheritDoc
*/
protected function getDetails(string $reference, array $args): ?array
{
if (str_ends_with($reference, 'Controller')) {
return [
'alias' => $args[0] ?? null,
'options' => $args[1] ?? null,
'sourceMethods' => $this->sourceMethods,
];
}
if ($reference === ComponentRegistry::class) {
return [
'alias' => $args[0] ?? null,
'options' => $args[1] ?? null,
'sourceMethods' => $this->componentRegistryMethods,
];
}

return null;
}
}
15 changes: 8 additions & 7 deletions src/Rule/LoadObjectExistsCakeClassRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use CakeDC\PHPStan\Rule\Traits\ParseClassNameFromArgTrait;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\Scope;
Expand Down Expand Up @@ -73,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array
$details['options']
);
}
if ($this->getTargetClassName($inputClassName)) {
if ($inputClassName === null || $this->getTargetClassName($inputClassName)) {
return [];
}

Expand All @@ -92,12 +93,11 @@ public function processNode(Node $node, Scope $scope): array
/**
* @param \PhpParser\Node\Scalar\String_ $nameArg
* @param \PhpParser\Node\Arg|null $options
* @return string
* @return string|null
*/
protected function getInputClassName(String_ $nameArg, ?Arg $options): string
protected function getInputClassName(String_ $nameArg, ?Arg $options): ?string
{
$className = $nameArg->value;

if (
$options === null
|| !$options->value instanceof Node\Expr\Array_
Expand All @@ -112,10 +112,11 @@ protected function getInputClassName(String_ $nameArg, ?Arg $options): string
) {
continue;
}
$name = $this->parseClassNameFromExprTrait($item->value);
if ($name !== null) {
return $name;
if ($item->value instanceof ConstFetch && $item->value->name->toString() === 'null') {
return $className;
}

return $this->parseClassNameFromExprTrait($item->value);
}

return $className;
Expand Down
84 changes: 84 additions & 0 deletions src/Rule/Mailer/GetMailerExistsClassRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);

/**
* Copyright 2024, Cake Development Corporation (https://www.cakedc.com)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2024, Cake Development Corporation (https://www.cakedc.com)
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/

namespace CakeDC\PHPStan\Rule\Mailer;

use CakeDC\PHPStan\Utility\CakeNameRegistry;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ThisType;

class GetMailerExistsClassRule implements Rule
{
/**
* @var string
*/
protected string $identifier = 'cake.getMailer.existClass';

/**
* @return string
*/
public function getNodeType(): string
{
return MethodCall::class;
}

/**
* @param \PhpParser\Node $node
* @param \PHPStan\Analyser\Scope $scope
* @return array<\PHPStan\Rules\RuleError>
*/
public function processNode(Node $node, Scope $scope): array
{
assert($node instanceof MethodCall);
if (
!$node->name instanceof Node\Identifier
|| $node->name->name !== 'getMailer'
) {
return [];
}

$args = $node->getArgs();
if (!isset($args[0])) {
return [];
}
$value = $args[0]->value;
if (!$value instanceof String_) {
return [];
}
$callerType = $scope->getType($node->var);
if (!$callerType instanceof ThisType) {
return [];
}
$reflection = $callerType->getClassReflection();

if (CakeNameRegistry::getMailerClassName($value->value)) {
return [];
}

return [
RuleErrorBuilder::message(sprintf(
'Call to %s::%s could not find the class for "%s"',
$reflection->getName(),
$node->name->name,
$value->value,
))
->identifier($this->identifier)
->build(),
];
}
}
48 changes: 48 additions & 0 deletions src/Rule/Model/DisallowEntityArrayAccessRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);

namespace CakeDC\PHPStan\Rule\Model;

use Cake\Datasource\EntityInterface;
use PhpParser\Node;
use PhpParser\Node\Expr\ArrayDimFetch;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ObjectType;

class DisallowEntityArrayAccessRule implements Rule
{
/**
* @return string
*/
public function getNodeType(): string
{
return ArrayDimFetch::class;
}

/**
* @param \PhpParser\Node $node
* @param \PHPStan\Analyser\Scope $scope
* @return array<\PHPStan\Rules\RuleError>
* @throws \PHPStan\ShouldNotHappenException
* @throws \PHPStan\Reflection\MissingMethodFromReflectionException
*/
public function processNode(Node $node, Scope $scope): array
{
assert($node instanceof ArrayDimFetch);
$type = $scope->getType($node->var);
if (!$type instanceof ObjectType || !is_a($type->getClassName(), EntityInterface::class, true)) {
return [];
}

return [
RuleErrorBuilder::message(sprintf(
'Array access to entity to %s is not allowed, access as object instead',
$type->getClassName(),
))
->identifier('cake.entity.arrayAccess')
->build(),
];
}
}
29 changes: 29 additions & 0 deletions src/Traits/IsFromTargetTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);

namespace CakeDC\PHPStan\Traits;

use PHPStan\Reflection\ClassReflection;

trait IsFromTargetTrait
{
/**
* @param \PHPStan\Reflection\ClassReflection $reflection
* @return bool
*/
protected function isFromTargetTrait(ClassReflection $reflection, string $targetTrait): bool
{
foreach ($reflection->getTraits() as $trait) {
if ($trait->getName() === $targetTrait) {
return true;
}
}
foreach ($reflection->getParents() as $parent) {
if ($this->isFromTargetTrait($parent, $targetTrait)) {
return true;
}
}

return false;
}
}
Loading

0 comments on commit e7bb4a4

Please sign in to comment.