Skip to content

Commit e7bb4a4

Browse files
authored
Merge pull request #38 from CakeDC/feature/new-rules-2410
Feature/new rules 2410
2 parents aa2e842 + 062a6d2 commit e7bb4a4

20 files changed

+610
-45
lines changed

.semver

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
22
:major: 3
3-
:minor: 1
4-
:patch: 2
3+
:minor: 2
4+
:patch: 0
55
:special: ''

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ Features included:
9090
1. Provide correct return type for `Cake\ORM\Table::saveManyOrFail` based on the first argument passed
9191
1. Provide correct return type for `Cake\ORM\Table::deleteMany` based on the first argument passed
9292
1. Provide correct return type for `Cake\ORM\Table::deleteManyOrFail` based on the first argument passed
93-
93+
1. Provide correct return type for `Cake\ORM\Locator\LocatorAwareTrait::fetchTable` based on the first argument passed
94+
1. Provide correct return type for `Cake\Mailer\MailerAwareTrait::getMailer` based on the first argument passed
9495

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

132133
### AddBehaviorExistsClassRule
133-
This rule check if the target behavior has a valid table class when calling to Table::addBehavior and BehaviorRegistry::load.
134+
This rule check if the target behavior has a valid class when calling to Table::addBehavior and BehaviorRegistry::load.
135+
136+
### DisallowEntityArrayAccessRule
137+
This rule disallow array access to entity in favor of object notation, is easier to detect a wrong property and to refactor code.
138+
139+
### GetMailerExistsClassRule
140+
This rule check if the target mailer is a valid class when calling to Cake\Mailer\MailerAwareTrait::getMailer.
141+
142+
### LoadComponentExistsClassRule
143+
This rule check if the target component has a valid class when calling to Controller::loadComponent and ComponentRegistry::load.
134144

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

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

151+
To enable this rule update your phpstan.neon with:
152+
153+
```
154+
parameters:
155+
cakeDC:
156+
disallowEntityArrayAccessRule: true
157+
```
158+
141159
### How to disable a rule
142160
Each rule has a parameter in cakeDC 'namespace' to enable or disable, it is the same name of the
143161
rule with first letter in lowercase.

phpstan.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ parameters:
77
level: max
88
checkGenericClassInNonGenericObjectType: false
99
treatPhpDocTypesAsCertain: false
10+
cakeDC:
11+
disallowEntityArrayAccessRule: true

rules.neon

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,36 @@ parameters:
55
addBehaviorExistsClassRule: true
66
tableGetMatchOptionsTypesRule: true
77
ormSelectQueryFindMatchOptionsTypesRule: true
8-
8+
disallowEntityArrayAccessRule: false
9+
getMailerExistsClassRule: true
10+
loadComponentExistsClassRule: true
911
parametersSchema:
1012
cakeDC: structure([
1113
addAssociationExistsTableClassRule: anyOf(bool(), arrayOf(bool()))
1214
addAssociationMatchOptionsTypesRule: anyOf(bool(), arrayOf(bool()))
1315
addBehaviorExistsClassRule: anyOf(bool(), arrayOf(bool()))
1416
tableGetMatchOptionsTypesRule: anyOf(bool(), arrayOf(bool()))
1517
ormSelectQueryFindMatchOptionsTypesRule: anyOf(bool(), arrayOf(bool()))
18+
disallowEntityArrayAccessRule: anyOf(bool(), arrayOf(bool()))
19+
getMailerExistsClassRule: anyOf(bool(), arrayOf(bool()))
20+
loadComponentExistsClassRule: anyOf(bool(), arrayOf(bool()))
1621
])
1722

1823
conditionalTags:
1924
CakeDC\PHPStan\Visitor\AddAssociationSetClassNameVisitor:
2025
phpstan.parser.richParserNodeVisitor: %cakeDC.addAssociationExistsTableClassRule%
26+
CakeDC\PHPStan\Rule\Controller\LoadComponentExistsClassRule:
27+
phpstan.rules.rule: %cakeDC.loadComponentExistsClassRule%
2128
CakeDC\PHPStan\Rule\Model\AddAssociationExistsTableClassRule:
2229
phpstan.rules.rule: %cakeDC.addAssociationExistsTableClassRule%
2330
CakeDC\PHPStan\Rule\Model\AddAssociationMatchOptionsTypesRule:
2431
phpstan.rules.rule: %cakeDC.addAssociationMatchOptionsTypesRule%
2532
CakeDC\PHPStan\Rule\Model\AddBehaviorExistsClassRule:
2633
phpstan.rules.rule: %cakeDC.addBehaviorExistsClassRule%
34+
CakeDC\PHPStan\Rule\Model\DisallowEntityArrayAccessRule:
35+
phpstan.rules.rule: %cakeDC.disallowEntityArrayAccessRule%
36+
CakeDC\PHPStan\Rule\Mailer\GetMailerExistsClassRule:
37+
phpstan.rules.rule: %cakeDC.getMailerExistsClassRule%
2738
CakeDC\PHPStan\Rule\Model\TableGetMatchOptionsTypesRule:
2839
phpstan.rules.rule: %cakeDC.tableGetMatchOptionsTypesRule%
2940
CakeDC\PHPStan\Rule\Model\OrmSelectQueryFindMatchOptionsTypesRule:
@@ -32,12 +43,18 @@ conditionalTags:
3243
services:
3344
-
3445
class: CakeDC\PHPStan\Visitor\AddAssociationSetClassNameVisitor
46+
-
47+
class: CakeDC\PHPStan\Rule\Controller\LoadComponentExistsClassRule
3548
-
3649
class: CakeDC\PHPStan\Rule\Model\AddAssociationExistsTableClassRule
3750
-
3851
class: CakeDC\PHPStan\Rule\Model\AddAssociationMatchOptionsTypesRule
3952
-
4053
class: CakeDC\PHPStan\Rule\Model\AddBehaviorExistsClassRule
54+
-
55+
class: CakeDC\PHPStan\Rule\Model\DisallowEntityArrayAccessRule
56+
-
57+
class: CakeDC\PHPStan\Rule\Mailer\GetMailerExistsClassRule
4158
-
4259
class: CakeDC\PHPStan\Rule\Model\TableGetMatchOptionsTypesRule
4360
-
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* Copyright 2024, Cake Development Corporation (https://www.cakedc.com)
6+
*
7+
* Licensed under The MIT License
8+
* Redistributions of files must retain the above copyright notice.
9+
*
10+
* @copyright Copyright 2024, Cake Development Corporation (https://www.cakedc.com)
11+
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
12+
*/
13+
14+
namespace CakeDC\PHPStan\Rule\Controller;
15+
16+
use Cake\Controller\ComponentRegistry;
17+
use CakeDC\PHPStan\Rule\LoadObjectExistsCakeClassRule;
18+
use CakeDC\PHPStan\Utility\CakeNameRegistry;
19+
20+
class LoadComponentExistsClassRule extends LoadObjectExistsCakeClassRule
21+
{
22+
/**
23+
* @var string
24+
*/
25+
protected string $identifier = 'cake.loadComponent.existClass';
26+
27+
/**
28+
* @var array<string>
29+
*/
30+
protected array $sourceMethods = [
31+
'loadComponent',
32+
];
33+
34+
/**
35+
* @var array<string>
36+
*/
37+
protected array $componentRegistryMethods = [
38+
'load',
39+
];
40+
41+
/**
42+
* @inheritDoc
43+
*/
44+
protected function getTargetClassName(string $name): ?string
45+
{
46+
return CakeNameRegistry::getComponentClassName($name);
47+
}
48+
49+
/**
50+
* @inheritDoc
51+
*/
52+
protected function getDetails(string $reference, array $args): ?array
53+
{
54+
if (str_ends_with($reference, 'Controller')) {
55+
return [
56+
'alias' => $args[0] ?? null,
57+
'options' => $args[1] ?? null,
58+
'sourceMethods' => $this->sourceMethods,
59+
];
60+
}
61+
if ($reference === ComponentRegistry::class) {
62+
return [
63+
'alias' => $args[0] ?? null,
64+
'options' => $args[1] ?? null,
65+
'sourceMethods' => $this->componentRegistryMethods,
66+
];
67+
}
68+
69+
return null;
70+
}
71+
}

src/Rule/LoadObjectExistsCakeClassRule.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use CakeDC\PHPStan\Rule\Traits\ParseClassNameFromArgTrait;
1717
use PhpParser\Node;
1818
use PhpParser\Node\Arg;
19+
use PhpParser\Node\Expr\ConstFetch;
1920
use PhpParser\Node\Expr\MethodCall;
2021
use PhpParser\Node\Scalar\String_;
2122
use PHPStan\Analyser\Scope;
@@ -73,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array
7374
$details['options']
7475
);
7576
}
76-
if ($this->getTargetClassName($inputClassName)) {
77+
if ($inputClassName === null || $this->getTargetClassName($inputClassName)) {
7778
return [];
7879
}
7980

@@ -92,12 +93,11 @@ public function processNode(Node $node, Scope $scope): array
9293
/**
9394
* @param \PhpParser\Node\Scalar\String_ $nameArg
9495
* @param \PhpParser\Node\Arg|null $options
95-
* @return string
96+
* @return string|null
9697
*/
97-
protected function getInputClassName(String_ $nameArg, ?Arg $options): string
98+
protected function getInputClassName(String_ $nameArg, ?Arg $options): ?string
9899
{
99100
$className = $nameArg->value;
100-
101101
if (
102102
$options === null
103103
|| !$options->value instanceof Node\Expr\Array_
@@ -112,10 +112,11 @@ protected function getInputClassName(String_ $nameArg, ?Arg $options): string
112112
) {
113113
continue;
114114
}
115-
$name = $this->parseClassNameFromExprTrait($item->value);
116-
if ($name !== null) {
117-
return $name;
115+
if ($item->value instanceof ConstFetch && $item->value->name->toString() === 'null') {
116+
return $className;
118117
}
118+
119+
return $this->parseClassNameFromExprTrait($item->value);
119120
}
120121

121122
return $className;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* Copyright 2024, Cake Development Corporation (https://www.cakedc.com)
6+
*
7+
* Licensed under The MIT License
8+
* Redistributions of files must retain the above copyright notice.
9+
*
10+
* @copyright Copyright 2024, Cake Development Corporation (https://www.cakedc.com)
11+
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
12+
*/
13+
14+
namespace CakeDC\PHPStan\Rule\Mailer;
15+
16+
use CakeDC\PHPStan\Utility\CakeNameRegistry;
17+
use PhpParser\Node;
18+
use PhpParser\Node\Expr\MethodCall;
19+
use PhpParser\Node\Scalar\String_;
20+
use PHPStan\Analyser\Scope;
21+
use PHPStan\Rules\Rule;
22+
use PHPStan\Rules\RuleErrorBuilder;
23+
use PHPStan\Type\ThisType;
24+
25+
class GetMailerExistsClassRule implements Rule
26+
{
27+
/**
28+
* @var string
29+
*/
30+
protected string $identifier = 'cake.getMailer.existClass';
31+
32+
/**
33+
* @return string
34+
*/
35+
public function getNodeType(): string
36+
{
37+
return MethodCall::class;
38+
}
39+
40+
/**
41+
* @param \PhpParser\Node $node
42+
* @param \PHPStan\Analyser\Scope $scope
43+
* @return array<\PHPStan\Rules\RuleError>
44+
*/
45+
public function processNode(Node $node, Scope $scope): array
46+
{
47+
assert($node instanceof MethodCall);
48+
if (
49+
!$node->name instanceof Node\Identifier
50+
|| $node->name->name !== 'getMailer'
51+
) {
52+
return [];
53+
}
54+
55+
$args = $node->getArgs();
56+
if (!isset($args[0])) {
57+
return [];
58+
}
59+
$value = $args[0]->value;
60+
if (!$value instanceof String_) {
61+
return [];
62+
}
63+
$callerType = $scope->getType($node->var);
64+
if (!$callerType instanceof ThisType) {
65+
return [];
66+
}
67+
$reflection = $callerType->getClassReflection();
68+
69+
if (CakeNameRegistry::getMailerClassName($value->value)) {
70+
return [];
71+
}
72+
73+
return [
74+
RuleErrorBuilder::message(sprintf(
75+
'Call to %s::%s could not find the class for "%s"',
76+
$reflection->getName(),
77+
$node->name->name,
78+
$value->value,
79+
))
80+
->identifier($this->identifier)
81+
->build(),
82+
];
83+
}
84+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace CakeDC\PHPStan\Rule\Model;
5+
6+
use Cake\Datasource\EntityInterface;
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\ArrayDimFetch;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
use PHPStan\Type\ObjectType;
13+
14+
class DisallowEntityArrayAccessRule implements Rule
15+
{
16+
/**
17+
* @return string
18+
*/
19+
public function getNodeType(): string
20+
{
21+
return ArrayDimFetch::class;
22+
}
23+
24+
/**
25+
* @param \PhpParser\Node $node
26+
* @param \PHPStan\Analyser\Scope $scope
27+
* @return array<\PHPStan\Rules\RuleError>
28+
* @throws \PHPStan\ShouldNotHappenException
29+
* @throws \PHPStan\Reflection\MissingMethodFromReflectionException
30+
*/
31+
public function processNode(Node $node, Scope $scope): array
32+
{
33+
assert($node instanceof ArrayDimFetch);
34+
$type = $scope->getType($node->var);
35+
if (!$type instanceof ObjectType || !is_a($type->getClassName(), EntityInterface::class, true)) {
36+
return [];
37+
}
38+
39+
return [
40+
RuleErrorBuilder::message(sprintf(
41+
'Array access to entity to %s is not allowed, access as object instead',
42+
$type->getClassName(),
43+
))
44+
->identifier('cake.entity.arrayAccess')
45+
->build(),
46+
];
47+
}
48+
}

src/Traits/IsFromTargetTrait.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace CakeDC\PHPStan\Traits;
5+
6+
use PHPStan\Reflection\ClassReflection;
7+
8+
trait IsFromTargetTrait
9+
{
10+
/**
11+
* @param \PHPStan\Reflection\ClassReflection $reflection
12+
* @return bool
13+
*/
14+
protected function isFromTargetTrait(ClassReflection $reflection, string $targetTrait): bool
15+
{
16+
foreach ($reflection->getTraits() as $trait) {
17+
if ($trait->getName() === $targetTrait) {
18+
return true;
19+
}
20+
}
21+
foreach ($reflection->getParents() as $parent) {
22+
if ($this->isFromTargetTrait($parent, $targetTrait)) {
23+
return true;
24+
}
25+
}
26+
27+
return false;
28+
}
29+
}

0 commit comments

Comments
 (0)