Skip to content

Commit 8777ef9

Browse files
Merge pull request #49 from sabbelasichon/add-validator-resolver-rule
Add validator resolver rule
2 parents 1b86945 + 642ea2b commit 8777ef9

12 files changed

+543
-3
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
/.idea
55
/index.php
66
/public
7-
/var
7+
/var
8+
.phpunit.result.cache

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"prefer-stable": true,
88
"require": {
99
"php": "^7.2 || ^8.0",
10-
"phpstan/phpstan": "^1.6",
10+
"phpstan/phpstan": "^1.7",
1111
"nikic/php-parser": ">= 4.13",
1212
"typo3/cms-core": "^8.7 || ^9.5 || ^10.4 || ^11.2",
1313
"typo3/cms-extbase": "^8.7 || ^9.5 || ^10.4 || ^11.2"

extension.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ services:
5151
siteGetAttributeMapping: %typo3.siteGetAttributeMapping%
5252
tags:
5353
- phpstan.rules.rule
54+
-
55+
class: SaschaEgerer\PhpstanTypo3\Rule\ValidatorResolverOptionsRule
56+
tags:
57+
- phpstan.rules.rule
5458
-
5559
class: SaschaEgerer\PhpstanTypo3\Type\RepositoryQueryDynamicReturnTypeExtension
5660
tags:
@@ -71,6 +75,9 @@ services:
7175
siteGetAttributeMapping: %typo3.siteGetAttributeMapping%
7276
tags:
7377
- phpstan.broker.dynamicMethodReturnTypeExtension
78+
-
79+
class: SaschaEgerer\PhpstanTypo3\Service\ValidatorClassNameResolver
80+
7481
parameters:
7582
bootstrapFiles:
7683
- phpstan.bootstrap.php

phpstan.neon

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ parameters:
1111
- src
1212
- tests
1313
reportUnmatchedIgnoredErrors: false
14+
excludePaths:
15+
- '*tests/*/Fixture/*'
16+
- '*tests/*/Source/*'
1417
ignoreErrors:
1518
-
1619
message: '#^Class TYPO3\\CMS\\Core\\Context\\[a-zA-Z]* not found\.#'
17-
path: %currentWorkingDirectory%/src/Type/ContextDynamicReturnTypeExtension.php
20+
path: src/Type/ContextDynamicReturnTypeExtension.php
21+
-
22+
message: "#^Calling PHPStan\\\\Reflection\\\\InitializerExprTypeResolver\\:\\:getClassConstFetchType\\(\\) is not covered by backward compatibility promise\\. The method might change in a minor PHPStan version\\.$#"
23+
count: 1
24+
path: src/Rule/ValidatorResolverOptionsRule.php
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SaschaEgerer\PhpstanTypo3\Rule;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Arg;
7+
use PhpParser\Node\Expr\Array_;
8+
use PhpParser\Node\Expr\ArrayItem;
9+
use PhpParser\Node\Expr\ClassConstFetch;
10+
use PhpParser\Node\Expr\MethodCall;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Reflection\ClassReflection;
13+
use PHPStan\Reflection\InitializerExprTypeResolver;
14+
use PHPStan\Reflection\Php\PhpPropertyReflection;
15+
use PHPStan\Reflection\PropertyReflection;
16+
use PHPStan\Rules\Rule;
17+
use PHPStan\Rules\RuleErrorBuilder;
18+
use PHPStan\Type\Constant\ConstantArrayType;
19+
use PHPStan\Type\Constant\ConstantBooleanType;
20+
use PHPStan\Type\Constant\ConstantStringType;
21+
use PHPStan\Type\ObjectType;
22+
use PHPStan\Type\Type;
23+
use SaschaEgerer\PhpstanTypo3\Rule\ValueObject\ValidatorOptionsConfiguration;
24+
use SaschaEgerer\PhpstanTypo3\Service\ValidatorClassNameResolver;
25+
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
26+
use TYPO3\CMS\Extbase\Validation\ValidatorResolver;
27+
28+
/**
29+
* @implements Rule<MethodCall>
30+
*/
31+
final class ValidatorResolverOptionsRule implements Rule
32+
{
33+
34+
/** @var InitializerExprTypeResolver */
35+
private $initializerExprTypeResolver;
36+
37+
/** @var ValidatorClassNameResolver */
38+
private $validatorClassNameResolver;
39+
40+
public function __construct(InitializerExprTypeResolver $initializerExprTypeResolver, ValidatorClassNameResolver $validatorClassNameResolver)
41+
{
42+
$this->initializerExprTypeResolver = $initializerExprTypeResolver;
43+
$this->validatorClassNameResolver = $validatorClassNameResolver;
44+
}
45+
46+
public function getNodeType(): string
47+
{
48+
return MethodCall::class;
49+
}
50+
51+
/**
52+
* @param MethodCall $node
53+
*/
54+
public function processNode(Node $node, Scope $scope): array
55+
{
56+
if ($this->shouldSkip($node, $scope)) {
57+
return [];
58+
}
59+
60+
$validatorTypeArgument = $node->getArgs()[0] ?? null;
61+
$validatorOptionsArgument = $node->getArgs()[1] ?? null;
62+
63+
if ($validatorTypeArgument === null) {
64+
return [];
65+
}
66+
67+
$validatorType = $scope->getType($validatorTypeArgument->value);
68+
69+
try {
70+
$validatorClassName = $this->validatorClassNameResolver->resolve($validatorType);
71+
} catch (\TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException $e) {
72+
if ($validatorType instanceof ConstantStringType) {
73+
$validatorClassName = $validatorType->getValue();
74+
$message = sprintf('Could not create validator for "%s"', $validatorClassName);
75+
} else {
76+
$message = 'Could not create validator';
77+
}
78+
79+
return [
80+
RuleErrorBuilder::message($message)->build(),
81+
];
82+
}
83+
84+
if ($validatorClassName === null) {
85+
return [];
86+
}
87+
88+
$validatorObjectType = new ObjectType($validatorClassName);
89+
$validatorClassReflection = $validatorObjectType->getClassReflection();
90+
91+
if ( ! $validatorClassReflection instanceof ClassReflection) {
92+
return [];
93+
}
94+
95+
if (!$validatorClassReflection->isSubclassOf(AbstractValidator::class)) {
96+
return [];
97+
}
98+
99+
try {
100+
$supportedOptions = $validatorClassReflection->getProperty('supportedOptions', $scope);
101+
} catch (\PHPStan\Reflection\MissingPropertyFromReflectionException $e) {
102+
return [];
103+
}
104+
105+
$validatorOptionsConfiguration = $this->extractValidatorOptionsConfiguration($supportedOptions, $scope);
106+
$providedOptionsArray = $this->extractProvidedOptions($validatorOptionsArgument, $scope);
107+
108+
$unsupportedOptions = array_diff($providedOptionsArray, $validatorOptionsConfiguration->getSupportedOptions());
109+
$neededRequiredOptions = array_diff($validatorOptionsConfiguration->getRequiredOptions(), $providedOptionsArray);
110+
111+
$errors = [];
112+
113+
if ($neededRequiredOptions !== []) {
114+
foreach ($neededRequiredOptions as $neededRequiredOption) {
115+
$errorMessage = sprintf('Required validation option not set: %s', $neededRequiredOption);
116+
$errors[] = RuleErrorBuilder::message($errorMessage)->build();
117+
}
118+
}
119+
120+
if ($unsupportedOptions !== []) {
121+
$errorMessage = 'Unsupported validation option(s) found: ' . implode(', ', $unsupportedOptions);
122+
$errors[] = RuleErrorBuilder::message($errorMessage)->build();
123+
}
124+
125+
return $errors;
126+
}
127+
128+
private function shouldSkip(MethodCall $methodCall, Scope $scope): bool
129+
{
130+
$objectType = $scope->getType($methodCall->var);
131+
$validatorResolverType = new ObjectType(ValidatorResolver::class);
132+
133+
if ($validatorResolverType->isSuperTypeOf($objectType)->no()) {
134+
return true;
135+
}
136+
137+
if ( ! $methodCall->name instanceof Node\Identifier) {
138+
return true;
139+
}
140+
141+
return $methodCall->name->toString() !== 'createValidator';
142+
}
143+
144+
/**
145+
* @return string[]
146+
*/
147+
private function extractProvidedOptions(?Arg $validatorOptionsArgument, Scope $scope): array
148+
{
149+
if ( ! $validatorOptionsArgument instanceof Arg) {
150+
return [];
151+
}
152+
153+
$providedOptionsArray = [];
154+
155+
$validatorOptionsArgumentType = $scope->getType($validatorOptionsArgument->value);
156+
157+
if ( ! $validatorOptionsArgumentType instanceof ConstantArrayType) {
158+
return [];
159+
}
160+
161+
$keysArray = $validatorOptionsArgumentType->getKeysArray();
162+
163+
foreach ($keysArray->getValueTypes() as $valueType) {
164+
if ( ! ($valueType instanceof ConstantStringType)) {
165+
continue;
166+
}
167+
168+
$providedOptionsArray[] = $valueType->getValue();
169+
}
170+
171+
return $providedOptionsArray;
172+
}
173+
174+
private function extractValidatorOptionsConfiguration(PropertyReflection $supportedOptions, Scope $scope): ValidatorOptionsConfiguration
175+
{
176+
$collectedSupportedOptions = [];
177+
$collectedRequiredOptions = [];
178+
179+
if ( ! $supportedOptions instanceof PhpPropertyReflection) {
180+
return ValidatorOptionsConfiguration::empty();
181+
}
182+
183+
$defaultValues = $supportedOptions->getNativeReflection()->getDefaultValueExpr();
184+
185+
if ( ! $defaultValues instanceof Array_) {
186+
return ValidatorOptionsConfiguration::empty();
187+
}
188+
189+
foreach ($defaultValues->items as $defaultValue) {
190+
191+
if ( ! $defaultValue instanceof ArrayItem) {
192+
continue;
193+
}
194+
195+
if ($defaultValue->key === null) {
196+
continue;
197+
}
198+
199+
$supportedOptionKey = $this->resolveOptionKeyValue($defaultValue, $supportedOptions, $scope);
200+
201+
if ($supportedOptionKey === null) {
202+
continue;
203+
}
204+
205+
$collectedSupportedOptions[] = $supportedOptionKey;
206+
207+
$optionDefinition = $defaultValue->value;
208+
if ( ! $optionDefinition instanceof Array_) {
209+
continue;
210+
}
211+
212+
if ( ! isset($optionDefinition->items[3])) {
213+
continue;
214+
}
215+
216+
$requiredValueType = $scope->getType($optionDefinition->items[3]->value);
217+
218+
if (!$requiredValueType instanceof ConstantBooleanType) {
219+
continue;
220+
}
221+
222+
if (!$requiredValueType->getValue()) {
223+
continue;
224+
}
225+
226+
$collectedRequiredOptions[] = $supportedOptionKey;
227+
}
228+
229+
return new ValidatorOptionsConfiguration($collectedSupportedOptions, $collectedRequiredOptions);
230+
}
231+
232+
private function resolveOptionKeyValue(ArrayItem $defaultValue, PhpPropertyReflection $supportedOptions, Scope $scope): ?string
233+
{
234+
if ($defaultValue->key === null) {
235+
return null;
236+
}
237+
238+
if ($defaultValue->key instanceof ClassConstFetch && $defaultValue->key->name instanceof Node\Identifier) {
239+
$keyType = $this->initializerExprTypeResolver->getClassConstFetchType(
240+
$defaultValue->key->class,
241+
$defaultValue->key->name->toString(),
242+
$supportedOptions->getDeclaringClass()->getName(),
243+
static function (\PhpParser\Node\Expr $expr) use ($scope): Type {
244+
return $scope->getType($expr);
245+
}
246+
);
247+
248+
if ($keyType instanceof ConstantStringType) {
249+
return $keyType->getValue();
250+
}
251+
252+
return null;
253+
}
254+
255+
$keyType = $scope->getType($defaultValue->key);
256+
257+
if ($keyType instanceof ConstantStringType) {
258+
return $keyType->getValue();
259+
}
260+
261+
return null;
262+
}
263+
264+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SaschaEgerer\PhpstanTypo3\Rule\ValueObject;
4+
5+
final class ValidatorOptionsConfiguration
6+
{
7+
8+
/** @var string[] */
9+
private $supportedOptions;
10+
11+
/** @var string[] */
12+
private $requriedOptions;
13+
14+
/**
15+
* @param string[] $supportedOptions
16+
* @param string[] $requriedOptions
17+
*/
18+
public function __construct(array $supportedOptions, array $requriedOptions)
19+
{
20+
$this->supportedOptions = $supportedOptions;
21+
$this->requriedOptions = $requriedOptions;
22+
}
23+
24+
public static function empty(): self
25+
{
26+
return new self([], []);
27+
}
28+
29+
/**
30+
* @return string[]
31+
*/
32+
public function getSupportedOptions(): array
33+
{
34+
return $this->supportedOptions;
35+
}
36+
37+
/**
38+
* @return string[]
39+
*/
40+
public function getRequiredOptions(): array
41+
{
42+
return $this->requriedOptions;
43+
}
44+
45+
}

0 commit comments

Comments
 (0)