├── stubs ├── DomainObjectInterface.stub ├── RepositoryInterface.stub ├── QueryResultInterface.stub ├── ObjectStorage.stub ├── QueryInterface.stub └── Repository.stub ├── src ├── Contract │ ├── ServiceMapFactory.php │ ├── ServiceDefinitionChecker.php │ └── ServiceMap.php ├── Service │ ├── NullServiceDefinitionChecker.php │ ├── ServiceDefinitionFileException.php │ ├── ValidatorClassNameResolver.php │ ├── FakeServiceMap.php │ ├── DefaultServiceMap.php │ ├── PrivateServiceAnalyzer.php │ ├── ServiceDefinition.php │ ├── PrototypeServiceDefinitionChecker.php │ └── XmlServiceMapFactory.php ├── Rule │ ├── ValueObject │ │ └── ValidatorOptionsConfiguration.php │ ├── ContainerInterfacePrivateServiceRule.php │ ├── GeneralUtilityMakeInstancePrivateServiceRule.php │ ├── SiteAttributeValidationRule.php │ ├── ContextAspectValidationRule.php │ ├── RequestAttributeValidationRule.php │ └── ValidatorResolverOptionsRule.php └── Type │ ├── ValidatorResolverDynamicReturnTypeExtension.php │ ├── ObjectStorageDynamicReturnTypeExtension.php │ ├── DateTimeAspectGetDynamicReturnTypeExtension.php │ ├── GeneralUtilityGetIndpEnvDynamicReturnTypeExtension.php │ ├── UserAspectGetDynamicReturnTypeExtension.php │ ├── SiteDynamicReturnTypeExtension.php │ ├── ContextDynamicReturnTypeExtension.php │ ├── RequestDynamicReturnTypeExtension.php │ ├── PropertyMapperReturnTypeExtension.php │ └── MathUtilityTypeSpecifyingExtension.php ├── rules.neon ├── phpstan.bootstrap.php ├── LICENSE ├── phpstan.neon ├── composer.json ├── README.md └── extension.neon /stubs/DomainObjectInterface.stub: -------------------------------------------------------------------------------- 1 | 8 | * @extends \ArrayAccess 9 | */ 10 | interface QueryResultInterface extends \Countable, \Iterator, \ArrayAccess { 11 | } 12 | -------------------------------------------------------------------------------- /phpstan.bootstrap.php: -------------------------------------------------------------------------------- 1 | 7 | * @implements \Iterator 8 | * @phpstan-type ObjectStorageInternal array{obj: TEntity, inf: mixed} 9 | */ 10 | class ObjectStorage implements \Iterator, \ArrayAccess 11 | { 12 | /** 13 | * @param TEntity|string|int $value 14 | * @phpstan-return TEntity|null 15 | */ 16 | public function offsetGet(mixed $value); 17 | } 18 | -------------------------------------------------------------------------------- /src/Contract/ServiceMap.php: -------------------------------------------------------------------------------- 1 | getConstantStrings() === []) { 17 | return null; 18 | } 19 | 20 | return \TYPO3\CMS\Extbase\Validation\ValidatorClassNameResolver::resolve($type->getConstantStrings()[0]->getValue()); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/Service/FakeServiceMap.php: -------------------------------------------------------------------------------- 1 | > : QueryResultInterface) 15 | */ 16 | public function execute(bool $returnRawQueryResult = false); 17 | 18 | /** 19 | * @param mixed $constraint 20 | * @return \TYPO3\CMS\Extbase\Persistence\QueryInterface 21 | */ 22 | public function matching($constraint); 23 | 24 | /** 25 | * @return class-string 26 | */ 27 | public function getType(); 28 | } 29 | -------------------------------------------------------------------------------- /stubs/Repository.stub: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class Repository implements RepositoryInterface 9 | { 10 | /** 11 | * @phpstan-return QueryResultInterface 12 | */ 13 | public function findAll(); 14 | 15 | /** 16 | * @phpstan-param array $criteria 17 | * @phpstan-param array|null $orderBy 18 | * @phpstan-param 0|positive-int|null $limit 19 | * @phpstan-param 0|positive-int|null $offset 20 | * @phpstan-return QueryResultInterface 21 | */ 22 | public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Rule/ValueObject/ValidatorOptionsConfiguration.php: -------------------------------------------------------------------------------- 1 | supportedOptions = $supportedOptions; 21 | $this->requiredOptions = $requiredOptions; 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->requiredOptions; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Service/DefaultServiceMap.php: -------------------------------------------------------------------------------- 1 | serviceDefinitions = $serviceDefinitions; 21 | } 22 | 23 | public function getServiceDefinitions(): array 24 | { 25 | return $this->serviceDefinitions; 26 | } 27 | 28 | public function getServiceDefinitionById(string $id): ?ServiceDefinition 29 | { 30 | return $this->serviceDefinitions[$id] ?? null; 31 | } 32 | 33 | public function getServiceIdFromNode(Expr $node, Scope $scope): ?string 34 | { 35 | $strings = $scope->getType($node)->getConstantStrings(); 36 | return count($strings) === 1 ? $strings[0]->getValue() : null; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sascha Egerer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Service/PrivateServiceAnalyzer.php: -------------------------------------------------------------------------------- 1 | serviceMap = $symfonyServiceMap; 22 | } 23 | 24 | /** 25 | * @param MethodCall|StaticCall $node 26 | * 27 | * @return list 28 | */ 29 | public function analyze(Node $node, Scope $scope, ServiceDefinitionChecker $serviceDefinitionChecker, string $identifier): array 30 | { 31 | $serviceId = $this->serviceMap->getServiceIdFromNode($node->getArgs()[0]->value, $scope); 32 | 33 | if ($serviceId === null) { 34 | return []; 35 | } 36 | 37 | $serviceDefinition = $this->serviceMap->getServiceDefinitionById($serviceId); 38 | 39 | if ($serviceDefinition === null || $serviceDefinition->isPublic()) { 40 | return []; 41 | } 42 | 43 | if ($serviceDefinitionChecker->isPrototype($serviceDefinition, $node)) { 44 | return []; 45 | } 46 | 47 | return [ 48 | RuleErrorBuilder::message(sprintf('Service "%s" is private.', $serviceId))->identifier($identifier)->build(), 49 | ]; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/Type/ValidatorResolverDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'createValidator'; 29 | } 30 | 31 | public function getTypeFromMethodCall( 32 | MethodReflection $methodReflection, 33 | MethodCall $methodCall, 34 | Scope $scope 35 | ): Type 36 | { 37 | $argument = $methodCall->getArgs()[0] ?? null; 38 | 39 | if ($argument === null) { 40 | return $methodReflection->getVariants()[0]->getReturnType(); 41 | } 42 | 43 | $argumentValue = $argument->value; 44 | 45 | if (!($argumentValue instanceof ClassConstFetch)) { 46 | return $methodReflection->getVariants()[0]->getReturnType(); 47 | } 48 | /** @var Name $class */ 49 | $class = $argumentValue->class; 50 | 51 | return TypeCombinator::addNull(new ObjectType((string) $class)); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/Type/ObjectStorageDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'offsetGet'; 29 | } 30 | 31 | public function getTypeFromMethodCall( 32 | MethodReflection $methodReflection, 33 | MethodCall $methodCall, 34 | Scope $scope 35 | ): ?Type 36 | { 37 | $firstArgument = $methodCall->args[0] ?? null; 38 | 39 | if (!$firstArgument instanceof Arg) { 40 | return null; 41 | } 42 | 43 | $argumentType = $scope->getType($firstArgument->value); 44 | 45 | if ((new StringType())->isSuperTypeOf($argumentType)->yes()) { 46 | return $methodReflection->getVariants()[0]->getReturnType(); 47 | } 48 | 49 | if ((new IntegerType())->isSuperTypeOf($argumentType)->yes()) { 50 | return $methodReflection->getVariants()[0]->getReturnType(); 51 | } 52 | 53 | return new MixedType(); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpstan/phpstan-strict-rules/rules.neon 3 | - vendor/phpstan/phpstan-phpunit/extension.neon 4 | - extension.neon 5 | - tests/Unit/Type/data/context-get-aspect-return-types.neon 6 | - tests/Unit/Type/data/request-get-attribute-return-types.neon 7 | - tests/Unit/Type/data/site-get-attribute-return-types.neon 8 | 9 | parameters: 10 | level: 8 11 | paths: 12 | - src 13 | - tests 14 | reportUnmatchedIgnoredErrors: false 15 | excludePaths: 16 | - '*tests/*/Fixtures/*' 17 | - '*tests/*/Fixture/*' 18 | - '*tests/*/Source/*' 19 | - '*tests/*/data/*' 20 | ignoreErrors: 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 25 | - 26 | message: '#^Although PHPStan\\Reflection\\Php\\PhpPropertyReflection is covered by backward compatibility promise, this instanceof assumption might break because it''s not guaranteed to always stay the same\.$#' 27 | identifier: phpstanApi.instanceofAssumption 28 | count: 1 29 | path: src/Rule/ValidatorResolverOptionsRule.php 30 | - 31 | message: '#^Node attribute ''parent'' is no longer available\.$#' 32 | identifier: phpParser.nodeConnectingAttribute 33 | count: 2 34 | path: src/Type/MathUtilityTypeSpecifyingExtension.php 35 | -------------------------------------------------------------------------------- /src/Type/DateTimeAspectGetDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'get'; 28 | } 29 | 30 | public function getTypeFromMethodCall( 31 | MethodReflection $methodReflection, 32 | MethodCall $methodCall, 33 | Scope $scope 34 | ): ?Type 35 | { 36 | $firstArgument = $methodCall->args[0]; 37 | 38 | if (!$firstArgument instanceof Arg) { 39 | return null; 40 | } 41 | 42 | $argumentType = $scope->getType($firstArgument->value); 43 | 44 | if ($argumentType->getConstantStrings() !== []) { 45 | switch ($argumentType->getConstantStrings()[0]->getValue()) { 46 | case 'timestamp': 47 | case 'accessTime': 48 | return new IntegerType(); 49 | case 'iso': 50 | case 'timezone': 51 | return new StringType(); 52 | case 'full': 53 | return new ObjectType(DateTimeImmutable::class); 54 | } 55 | } 56 | 57 | return null; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "saschaegerer/phpstan-typo3", 3 | "description": "TYPO3 CMS class reflection extension for PHPStan", 4 | "keywords": [ 5 | "static analysis" 6 | ], 7 | "license": [ 8 | "MIT" 9 | ], 10 | "type": "phpstan-extension", 11 | "minimum-stability": "dev", 12 | "prefer-stable": true, 13 | "prefer-source": true, 14 | "require": { 15 | "php": "^8.2", 16 | "ext-simplexml": "*", 17 | "phpstan/phpstan": "^2.1", 18 | "typo3/cms-core": "^13.4.3", 19 | "typo3/cms-extbase": "^13.4.3", 20 | "bnf/phpstan-psr-container": "^1.0", 21 | "composer/semver": "^3.4", 22 | "ssch/typo3-debug-dump-pass": "^0.0.2" 23 | }, 24 | "require-dev": { 25 | "consistence-community/coding-standard": "^3.11", 26 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0", 27 | "php-parallel-lint/php-parallel-lint": "^1.4", 28 | "phing/phing": "^2.17", 29 | "phpstan/phpstan-strict-rules": "^2.0", 30 | "phpstan/phpstan-phpunit": "^2.0.3", 31 | "phpunit/phpunit": "^11.5" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "SaschaEgerer\\PhpstanTypo3\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "SaschaEgerer\\PhpstanTypo3\\Tests\\": "tests/" 41 | }, 42 | "files": [ 43 | "tests/Unit/Type/data/repository-stub-files.php", 44 | "tests/Unit/Type/QueryResultToArrayDynamicReturnTypeExtension/data/query-result-to-array.php" 45 | ] 46 | }, 47 | "extra": { 48 | "phpstan": { 49 | "includes": [ 50 | "extension.neon" 51 | ] 52 | } 53 | }, 54 | "config": { 55 | "allow-plugins": { 56 | "dealerdirect/phpcodesniffer-composer-installer": true, 57 | "typo3/class-alias-loader": true, 58 | "typo3/cms-composer-installers": true 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Type/GeneralUtilityGetIndpEnvDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'getIndpEnv'; 28 | } 29 | 30 | public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type 31 | { 32 | $firstArgument = $methodCall->args[0]; 33 | 34 | if (!$firstArgument instanceof Arg) { 35 | return null; 36 | } 37 | 38 | $argumentType = $scope->getType($firstArgument->value); 39 | 40 | if ($argumentType->getConstantStrings() === []) { 41 | return null; 42 | } 43 | $value = $argumentType->getConstantStrings()[0]->getValue(); 44 | 45 | if ($value === '_ARRAY') { 46 | return new ArrayType(new StringType(), new UnionType([new StringType(), new BooleanType()])); 47 | } 48 | 49 | if (in_array($value, ['TYPO3_SSL', 'TYPO3_PROXY', 'TYPO3_REV_PROXY'], true)) { 50 | return new BooleanType(); 51 | } 52 | 53 | return new StringType(); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/Service/ServiceDefinition.php: -------------------------------------------------------------------------------- 1 | id = $id; 36 | $this->class = $class; 37 | $this->public = $public; 38 | $this->synthetic = $synthetic; 39 | $this->alias = $alias; 40 | $this->hasConstructorArguments = $hasConstructorArguments; 41 | $this->hasMethodCalls = $hasMethodCalls; 42 | $this->hasTags = $hasTags; 43 | } 44 | 45 | public function getId(): string 46 | { 47 | return $this->id; 48 | } 49 | 50 | public function getClass(): ?string 51 | { 52 | return $this->class; 53 | } 54 | 55 | public function isPublic(): bool 56 | { 57 | return $this->public; 58 | } 59 | 60 | public function isSynthetic(): bool 61 | { 62 | return $this->synthetic; 63 | } 64 | 65 | public function getAlias(): ?string 66 | { 67 | return $this->alias; 68 | } 69 | 70 | public function isHasConstructorArguments(): bool 71 | { 72 | return $this->hasConstructorArguments; 73 | } 74 | 75 | public function isHasMethodCalls(): bool 76 | { 77 | return $this->hasMethodCalls; 78 | } 79 | 80 | public function isHasTags(): bool 81 | { 82 | return $this->hasTags; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/Type/UserAspectGetDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'get'; 29 | } 30 | 31 | public function getTypeFromMethodCall( 32 | MethodReflection $methodReflection, 33 | MethodCall $methodCall, 34 | Scope $scope 35 | ): ?Type 36 | { 37 | $firstArgument = $methodCall->args[0]; 38 | 39 | if (!$firstArgument instanceof Arg) { 40 | return null; 41 | } 42 | 43 | $argumentType = $scope->getType($firstArgument->value); 44 | 45 | if ($argumentType->getConstantStrings() !== []) { 46 | switch ($argumentType->getConstantStrings()[0]->getValue()) { 47 | case 'id': 48 | return IntegerRangeType::createAllGreaterThanOrEqualTo(0); 49 | case 'username': 50 | return new StringType(); 51 | case 'isLoggedIn': 52 | case 'isAdmin': 53 | return new BooleanType(); 54 | case 'groupIds': 55 | return new ArrayType(new IntegerType(), IntegerRangeType::fromInterval(-2, null)); 56 | case 'groupNames': 57 | return new ArrayType(new IntegerType(), new StringType()); 58 | } 59 | } 60 | 61 | return null; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/Type/SiteDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | */ 18 | private array $siteGetAttributeMapping; 19 | 20 | private TypeStringResolver $typeStringResolver; 21 | 22 | /** 23 | * @param array $siteGetAttributeMapping 24 | */ 25 | public function __construct(array $siteGetAttributeMapping, TypeStringResolver $typeStringResolver) 26 | { 27 | $this->siteGetAttributeMapping = $siteGetAttributeMapping; 28 | $this->typeStringResolver = $typeStringResolver; 29 | } 30 | 31 | public function getClass(): string 32 | { 33 | return Site::class; 34 | } 35 | 36 | public function getTypeFromMethodCall( 37 | MethodReflection $methodReflection, 38 | MethodCall $methodCall, 39 | Scope $scope 40 | ): Type 41 | { 42 | $argument = $methodCall->getArgs()[0] ?? null; 43 | 44 | if ($argument === null || !($argument->value instanceof String_)) { 45 | return $methodReflection->getVariants()[0]->getReturnType(); 46 | } 47 | 48 | if (isset($this->siteGetAttributeMapping[$argument->value->value])) { 49 | return $this->typeStringResolver->resolve($this->siteGetAttributeMapping[$argument->value->value]); 50 | } 51 | 52 | return $methodReflection->getVariants()[0]->getReturnType(); 53 | } 54 | 55 | public function isMethodSupported( 56 | MethodReflection $methodReflection 57 | ): bool 58 | { 59 | return $methodReflection->getName() === 'getAttribute'; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/Type/ContextDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | */ 18 | private array $contextApiGetAspectMapping; 19 | 20 | private TypeStringResolver $typeStringResolver; 21 | 22 | /** 23 | * @param array $contextApiGetAspectMapping 24 | */ 25 | public function __construct(array $contextApiGetAspectMapping, TypeStringResolver $typeStringResolver) 26 | { 27 | $this->contextApiGetAspectMapping = $contextApiGetAspectMapping; 28 | $this->typeStringResolver = $typeStringResolver; 29 | } 30 | 31 | public function getClass(): string 32 | { 33 | return Context::class; 34 | } 35 | 36 | public function isMethodSupported( 37 | MethodReflection $methodReflection 38 | ): bool 39 | { 40 | return $methodReflection->getName() === 'getAspect'; 41 | } 42 | 43 | public function getTypeFromMethodCall( 44 | MethodReflection $methodReflection, 45 | MethodCall $methodCall, 46 | Scope $scope 47 | ): Type 48 | { 49 | $argument = $methodCall->getArgs()[0] ?? null; 50 | 51 | if ($argument === null || !($argument->value instanceof String_)) { 52 | return $methodReflection->getVariants()[0]->getReturnType(); 53 | } 54 | 55 | if (isset($this->contextApiGetAspectMapping[$argument->value->value])) { 56 | return $this->typeStringResolver->resolve($this->contextApiGetAspectMapping[$argument->value->value]); 57 | } 58 | 59 | return $methodReflection->getVariants()[0]->getReturnType(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/Rule/ContainerInterfacePrivateServiceRule.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class ContainerInterfacePrivateServiceRule implements Rule 17 | { 18 | 19 | private PrivateServiceAnalyzer $privateServiceAnalyzer; 20 | 21 | public function __construct(PrivateServiceAnalyzer $privateServiceAnalyzer) 22 | { 23 | $this->privateServiceAnalyzer = $privateServiceAnalyzer; 24 | } 25 | 26 | public function getNodeType(): string 27 | { 28 | return MethodCall::class; 29 | } 30 | 31 | public function processNode(Node $node, Scope $scope): array 32 | { 33 | if ($this->shouldSkip($node, $scope)) { 34 | return []; 35 | } 36 | 37 | return $this->privateServiceAnalyzer->analyze( 38 | $node, 39 | $scope, 40 | new NullServiceDefinitionChecker(), 41 | 'phpstanTypo3.containerInterfacePrivateService' 42 | ); 43 | } 44 | 45 | private function shouldSkip(MethodCall $node, Scope $scope): bool 46 | { 47 | if (!$node->name instanceof Node\Identifier) { 48 | return true; 49 | } 50 | 51 | $methodCallArguments = $node->getArgs(); 52 | 53 | if (!isset($methodCallArguments[0])) { 54 | return true; 55 | } 56 | 57 | $methodCallName = $node->name->name; 58 | 59 | if ($methodCallName !== 'get') { 60 | return true; 61 | } 62 | 63 | $argType = $scope->getType($node->var); 64 | 65 | $isPsrContainerType = (new ObjectType('Psr\Container\ContainerInterface'))->isSuperTypeOf($argType); 66 | $isTestCaseType = (new ObjectType('TYPO3\TestingFramework\Core\Functional\FunctionalTestCase'))->isSuperTypeOf($argType); 67 | 68 | if ($isTestCaseType->yes()) { 69 | return true; 70 | } 71 | 72 | return !$isPsrContainerType->yes(); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/Rule/GeneralUtilityMakeInstancePrivateServiceRule.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class GeneralUtilityMakeInstancePrivateServiceRule implements Rule 17 | { 18 | 19 | private PrivateServiceAnalyzer $privateServiceAnalyzer; 20 | 21 | private PrototypeServiceDefinitionChecker $prototypeServiceDefinitionChecker; 22 | 23 | public function __construct(PrivateServiceAnalyzer $privateServiceAnalyzer, PrototypeServiceDefinitionChecker $prototypeServiceDefinitionChecker) 24 | { 25 | $this->privateServiceAnalyzer = $privateServiceAnalyzer; 26 | $this->prototypeServiceDefinitionChecker = $prototypeServiceDefinitionChecker; 27 | } 28 | 29 | public function getNodeType(): string 30 | { 31 | return StaticCall::class; 32 | } 33 | 34 | public function processNode(Node $node, Scope $scope): array 35 | { 36 | if ($this->shouldSkip($node)) { 37 | return []; 38 | } 39 | 40 | return $this->privateServiceAnalyzer->analyze( 41 | $node, 42 | $scope, 43 | $this->prototypeServiceDefinitionChecker, 44 | 'phpstanTypo3.generalUtilityMakeInstancePrivateService' 45 | ); 46 | } 47 | 48 | private function shouldSkip(StaticCall $node): bool 49 | { 50 | if (!$node->name instanceof Node\Identifier) { 51 | return true; 52 | } 53 | 54 | $methodCallArguments = $node->getArgs(); 55 | 56 | if (!isset($methodCallArguments[0])) { 57 | return true; 58 | } 59 | 60 | $methodCallName = $node->name->name; 61 | 62 | if ($methodCallName !== 'makeInstance') { 63 | return true; 64 | } 65 | 66 | if (count($methodCallArguments) > 1) { 67 | return true; 68 | } 69 | 70 | if (!$node->class instanceof Node\Name\FullyQualified) { 71 | return true; 72 | } 73 | 74 | return $node->class->toString() !== GeneralUtility::class; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/Rule/SiteAttributeValidationRule.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class SiteAttributeValidationRule implements Rule 19 | { 20 | 21 | /** @var array */ 22 | private array $siteGetAttributeMapping; 23 | 24 | /** 25 | * @param array $siteGetAttributeMapping 26 | */ 27 | public function __construct(array $siteGetAttributeMapping) 28 | { 29 | $this->siteGetAttributeMapping = $siteGetAttributeMapping; 30 | } 31 | 32 | public function getNodeType(): string 33 | { 34 | return MethodCall::class; 35 | } 36 | 37 | /** 38 | * @param Node\Expr\MethodCall $node 39 | */ 40 | public function processNode(Node $node, Scope $scope): array 41 | { 42 | if (!$node->name instanceof Identifier) { 43 | return []; 44 | } 45 | 46 | $methodReflection = $scope->getMethodReflection($scope->getType($node->var), $node->name->toString()); 47 | if ($methodReflection === null || $methodReflection->getName() !== 'getAttribute') { 48 | return []; 49 | } 50 | 51 | $declaringClass = $methodReflection->getDeclaringClass(); 52 | 53 | if ($declaringClass->getName() !== Site::class) { 54 | return []; 55 | } 56 | 57 | $argument = $node->getArgs()[0] ?? null; 58 | 59 | if (!($argument instanceof Arg) || !($argument->value instanceof String_)) { 60 | return []; 61 | } 62 | 63 | if (isset($this->siteGetAttributeMapping[$argument->value->value])) { 64 | return []; 65 | } 66 | 67 | return [ 68 | RuleErrorBuilder::message(sprintf( 69 | 'There is no site attribute "%s" configured so we can\'t figure out the exact type to return when calling %s::%s', 70 | $argument->value->value, 71 | $declaringClass->getDisplayName(), 72 | $methodReflection->getName() 73 | )) 74 | ->tip('You should add custom site attribute to the typo3.siteGetAttributeMapping setting.') 75 | ->identifier('phpstanTypo3.siteAttributeValidation') 76 | ->build(), 77 | ]; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/Service/PrototypeServiceDefinitionChecker.php: -------------------------------------------------------------------------------- 1 | reflectionProvider = $reflectionProvider; 19 | } 20 | 21 | public function isPrototype(ServiceDefinition $serviceDefinition, Node $node): bool 22 | { 23 | return !$serviceDefinition->isHasTags() && !$serviceDefinition->isHasMethodCalls() && $this->canBePrototypeClass($node); 24 | } 25 | 26 | private function extractFirstArgument(StaticCall $node): ?Node 27 | { 28 | if (!isset($node->args[0])) { 29 | return null; 30 | } 31 | 32 | if (!$node->args[0] instanceof Node\Arg) { 33 | return null; 34 | } 35 | 36 | return $node->args[0]->value; 37 | } 38 | 39 | private function canBePrototypeClass(Node $node): bool 40 | { 41 | if (!$node instanceof StaticCall) { 42 | return false; 43 | } 44 | 45 | $firstArgument = $this->extractFirstArgument($node); 46 | 47 | if (!$firstArgument instanceof ClassConstFetch) { 48 | return false; 49 | } 50 | 51 | if (!$firstArgument->class instanceof Node\Name) { 52 | return false; 53 | } 54 | 55 | $className = $firstArgument->class->toString(); 56 | 57 | if (!$this->reflectionProvider->hasClass($className)) { 58 | return false; 59 | } 60 | 61 | $classReflection = $this->reflectionProvider->getClass($className); 62 | 63 | if (!$classReflection->hasConstructor()) { 64 | return true; 65 | } 66 | 67 | $constructorMethod = $classReflection->getConstructor(); 68 | 69 | $constructorParameters = $constructorMethod->getVariants(); 70 | 71 | $hasRequiredParameter = false; 72 | foreach ($constructorParameters as $constructorParameter) { 73 | foreach ($constructorParameter->getParameters() as $parameter) { 74 | if ($parameter->isOptional()) { 75 | continue; 76 | } 77 | $hasRequiredParameter = true; 78 | } 79 | } 80 | 81 | return $hasRequiredParameter === false; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/Rule/ContextAspectValidationRule.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class ContextAspectValidationRule implements Rule 19 | { 20 | 21 | /** @var array */ 22 | private array $contextApiGetAspectMapping; 23 | 24 | /** 25 | * @param array $contextApiGetAspectMapping 26 | */ 27 | public function __construct(array $contextApiGetAspectMapping) 28 | { 29 | $this->contextApiGetAspectMapping = $contextApiGetAspectMapping; 30 | } 31 | 32 | public function getNodeType(): string 33 | { 34 | return MethodCall::class; 35 | } 36 | 37 | /** 38 | * @param Node\Expr\MethodCall $node 39 | */ 40 | public function processNode(Node $node, Scope $scope): array 41 | { 42 | if (!$node->name instanceof Identifier) { 43 | return []; 44 | } 45 | 46 | $methodReflection = $scope->getMethodReflection($scope->getType($node->var), $node->name->toString()); 47 | 48 | if ($methodReflection === null) { 49 | return []; 50 | } 51 | 52 | if (!in_array($methodReflection->getName(), ['getAspect', 'getPropertyFromAspect'], true)) { 53 | return []; 54 | } 55 | 56 | $declaringClass = $methodReflection->getDeclaringClass(); 57 | 58 | if ($declaringClass->getName() !== Context::class) { 59 | return []; 60 | } 61 | 62 | $argument = $node->getArgs()[0] ?? null; 63 | 64 | if (!($argument instanceof Arg) || !($argument->value instanceof String_)) { 65 | return []; 66 | } 67 | 68 | if (isset($this->contextApiGetAspectMapping[$argument->value->value])) { 69 | return []; 70 | } 71 | 72 | $ruleError = RuleErrorBuilder::message(sprintf( 73 | 'There is no aspect "%s" configured so we can\'t figure out the exact type to return when calling %s::%s', 74 | $argument->value->value, 75 | $declaringClass->getDisplayName(), 76 | $methodReflection->getName() 77 | )) 78 | ->tip('You should add custom aspects to the typo3.contextApiGetAspectMapping setting.') 79 | ->identifier('phpstanTypo3.contextAspectValidation') 80 | ->build(); 81 | 82 | return [$ruleError]; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/Rule/RequestAttributeValidationRule.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class RequestAttributeValidationRule implements Rule 19 | { 20 | 21 | /** @var array */ 22 | private array $requestGetAttributeMapping; 23 | 24 | /** 25 | * @param array $requestGetAttributeMapping 26 | */ 27 | public function __construct(array $requestGetAttributeMapping) 28 | { 29 | $this->requestGetAttributeMapping = $requestGetAttributeMapping; 30 | } 31 | 32 | public function getNodeType(): string 33 | { 34 | return MethodCall::class; 35 | } 36 | 37 | /** 38 | * @param Node\Expr\MethodCall $node 39 | */ 40 | public function processNode(Node $node, Scope $scope): array 41 | { 42 | if (!$node->name instanceof Identifier) { 43 | return []; 44 | } 45 | 46 | $methodReflection = $scope->getMethodReflection($scope->getType($node->var), $node->name->toString()); 47 | if ($methodReflection === null || $methodReflection->getName() !== 'getAttribute') { 48 | return []; 49 | } 50 | 51 | $declaringClass = $methodReflection->getDeclaringClass(); 52 | 53 | if (interface_exists(ServerRequestInterface::class)) { 54 | if (!$declaringClass->implementsInterface(ServerRequestInterface::class) 55 | && $declaringClass->getName() !== ServerRequestInterface::class) { 56 | return []; 57 | } 58 | } 59 | 60 | $argument = $node->getArgs()[0] ?? null; 61 | 62 | if (!($argument instanceof Arg) || !($argument->value instanceof String_)) { 63 | return []; 64 | } 65 | 66 | if (isset($this->requestGetAttributeMapping[$argument->value->value])) { 67 | return []; 68 | } 69 | 70 | $ruleError = RuleErrorBuilder::message(sprintf( 71 | 'There is no request attribute "%s" configured so we can\'t figure out the exact type to return when calling %s::%s', 72 | $argument->value->value, 73 | $declaringClass->getDisplayName(), 74 | $methodReflection->getName() 75 | )) 76 | ->tip('You should add custom request attribute to the typo3.requestGetAttributeMapping setting.') 77 | ->identifier('phpstanTypo3.requestAttributeValidation') 78 | ->build(); 79 | 80 | return [$ruleError]; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/Type/RequestDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | */ 19 | private array $requestGetAttributeMapping; 20 | 21 | private TypeStringResolver $typeStringResolver; 22 | 23 | /** 24 | * @param array $requestGetAttributeMapping 25 | */ 26 | public function __construct(array $requestGetAttributeMapping, TypeStringResolver $typeStringResolver) 27 | { 28 | $this->requestGetAttributeMapping = $requestGetAttributeMapping; 29 | $this->typeStringResolver = $typeStringResolver; 30 | } 31 | 32 | public function getClass(): string 33 | { 34 | if (!interface_exists(ServerRequestInterface::class)) { 35 | throw new \PHPStan\ShouldNotHappenException( 36 | 'The package "psr/http-message" is not installed, but should be.' 37 | ); 38 | } 39 | 40 | return ServerRequestInterface::class; 41 | } 42 | 43 | public function getTypeFromMethodCall( 44 | MethodReflection $methodReflection, 45 | MethodCall $methodCall, 46 | Scope $scope 47 | ): Type 48 | { 49 | $argument = $methodCall->getArgs()[0] ?? null; 50 | $defaultArgument = $methodCall->getArgs()[1] ?? null; 51 | 52 | if ($argument === null 53 | || !($argument->value instanceof String_) 54 | || !isset($this->requestGetAttributeMapping[$argument->value->value]) 55 | ) { 56 | 57 | $type = $methodReflection->getVariants()[0]->getReturnType(); 58 | 59 | if ($defaultArgument === null) { 60 | return $type; 61 | } 62 | 63 | return TypeCombinator::union($type, $scope->getType($defaultArgument->value)); 64 | } 65 | 66 | $type = $this->typeStringResolver->resolve($this->requestGetAttributeMapping[$argument->value->value]); 67 | 68 | if ($defaultArgument === null) { 69 | return TypeCombinator::addNull($type); 70 | } 71 | 72 | return TypeCombinator::union($type, $scope->getType($defaultArgument->value)); 73 | } 74 | 75 | public function isMethodSupported( 76 | MethodReflection $methodReflection 77 | ): bool 78 | { 79 | return $methodReflection->getName() === 'getAttribute'; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/Type/PropertyMapperReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | reflectionProvider = $reflectionProvider; 31 | } 32 | 33 | public function getClass(): string 34 | { 35 | return PropertyMapper::class; 36 | } 37 | 38 | public function isMethodSupported(MethodReflection $methodReflection): bool 39 | { 40 | return $methodReflection->getName() === 'convert'; 41 | } 42 | 43 | public function getTypeFromMethodCall( 44 | MethodReflection $methodReflection, 45 | MethodCall $methodCall, 46 | Scope $scope 47 | ): ?Type 48 | { 49 | $targetTypeArgument = $methodCall->getArgs()[1] ?? null; 50 | 51 | if ($targetTypeArgument === null) { 52 | return null; 53 | } 54 | 55 | $argumentValue = $targetTypeArgument->value; 56 | 57 | if ($argumentValue instanceof ClassConstFetch) { 58 | /** @var Name $class */ 59 | $class = $argumentValue->class; 60 | return TypeCombinator::addNull(new ObjectType((string) $class)); 61 | } 62 | 63 | if ($argumentValue instanceof String_) { 64 | return $this->createTypeFromString($argumentValue); 65 | } 66 | 67 | return null; 68 | } 69 | 70 | private function createTypeFromString(String_ $node): ?Type 71 | { 72 | if ($node->value === 'array') { 73 | return TypeCombinator::addNull(new ArrayType(new MixedType(), new MixedType())); 74 | } 75 | 76 | if ($node->value === 'string') { 77 | return TypeCombinator::addNull(new StringType()); 78 | } 79 | 80 | if ($node->value === 'boolean') { 81 | return TypeCombinator::addNull(new BooleanType()); 82 | } 83 | 84 | if ($node->value === 'integer') { 85 | return TypeCombinator::addNull(new IntegerType()); 86 | } 87 | 88 | return $this->createTypeFromClassNameString($node); 89 | } 90 | 91 | private function createTypeFromClassNameString(String_ $node): ?Type 92 | { 93 | $classLikeName = $node->value; 94 | 95 | // remove leading slash 96 | $classLikeName = ltrim($classLikeName, '\\'); 97 | if ($classLikeName === '') { 98 | return null; 99 | } 100 | 101 | if (!$this->reflectionProvider->hasClass($classLikeName)) { 102 | return null; 103 | } 104 | 105 | $classReflection = $this->reflectionProvider->getClass($classLikeName); 106 | if ($classReflection->getName() !== $classLikeName) { 107 | return null; 108 | } 109 | 110 | // possibly string 111 | if (ctype_lower($classLikeName[0])) { 112 | return null; 113 | } 114 | 115 | return TypeCombinator::addNull(new ObjectType($classLikeName)); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/Service/XmlServiceMapFactory.php: -------------------------------------------------------------------------------- 1 | containerXmlPath = $containerXmlPath; 17 | } 18 | 19 | public function create(): ServiceMap 20 | { 21 | if ($this->containerXmlPath === null) { 22 | return new FakeServiceMap(); 23 | } 24 | 25 | if (!file_exists($this->containerXmlPath)) { 26 | throw \SaschaEgerer\PhpstanTypo3\Service\ServiceDefinitionFileException::notFound($this->containerXmlPath); 27 | } 28 | 29 | $xml = @simplexml_load_file($this->containerXmlPath); 30 | 31 | if ($xml === false) { 32 | throw \SaschaEgerer\PhpstanTypo3\Service\ServiceDefinitionFileException::parseError($this->containerXmlPath); 33 | } 34 | 35 | /** @var ServiceDefinition[] $serviceDefinitions */ 36 | $serviceDefinitions = []; 37 | /** @var ServiceDefinition[] $aliases */ 38 | $aliases = []; 39 | foreach ($xml->services->service as $def) { 40 | /** @var SimpleXMLElement $attrs */ 41 | $attrs = $def->attributes(); 42 | if (!isset($attrs->id)) { 43 | continue; 44 | } 45 | 46 | $tags = $this->createTags($def); 47 | 48 | if (in_array('container.excluded', $tags, true)) { 49 | continue; 50 | } 51 | 52 | $serviceDefinition = new ServiceDefinition( 53 | strpos((string) $attrs->id, '.') === 0 ? substr((string) $attrs->id, 1) : (string) $attrs->id, 54 | isset($attrs->class) ? (string) $attrs->class : null, 55 | isset($attrs->public) && (string) $attrs->public === 'true', 56 | isset($attrs->synthetic) && (string) $attrs->synthetic === 'true', 57 | isset($attrs->alias) ? (string) $attrs->alias : null, 58 | isset($def->argument), 59 | isset($def->call), 60 | isset($def->tag), 61 | ); 62 | 63 | if ($serviceDefinition->getAlias() !== null) { 64 | $aliases[] = $serviceDefinition; 65 | } else { 66 | $serviceDefinitions[$serviceDefinition->getId()] = $serviceDefinition; 67 | } 68 | } 69 | foreach ($aliases as $serviceDefinition) { 70 | $alias = $serviceDefinition->getAlias(); 71 | if ($alias !== null && !isset($serviceDefinitions[$alias])) { 72 | continue; 73 | } 74 | $id = $serviceDefinition->getId(); 75 | $serviceDefinitions[$id] = new ServiceDefinition( 76 | $id, 77 | $serviceDefinitions[$alias]->getClass(), 78 | $serviceDefinition->isPublic(), 79 | $serviceDefinition->isSynthetic(), 80 | $alias, 81 | $serviceDefinition->isHasConstructorArguments(), 82 | $serviceDefinition->isHasMethodCalls(), 83 | $serviceDefinition->isHasTags() 84 | ); 85 | } 86 | 87 | return new DefaultServiceMap($serviceDefinitions); 88 | } 89 | 90 | /** 91 | * @return string[] 92 | */ 93 | private function createTags(?SimpleXMLElement $def): array 94 | { 95 | if (!isset($def->tag)) { 96 | return []; 97 | } 98 | 99 | $tagNames = []; 100 | 101 | foreach ($def->tag as $tag) { 102 | $attributes = $tag->attributes(); 103 | if (!isset($attributes->name)) { 104 | continue; 105 | } 106 | $tagNames[] = (string) $attributes->name; 107 | } 108 | 109 | return $tagNames; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPStan TYPO3 extensions and rules 2 | 3 | TYPO3 CMS class reflection extension for PHPStan & framework-specific rules. 4 | 5 | --- 6 | 7 | ## 🚀 Want to work on projects like this? 8 | 9 | We're hiring! Join us and help shape the future of TYPO3 development. 10 | 11 | 👉 [Check out our open positions](https://www.flowd.de/jobs.html) 12 | 13 | --- 14 | 15 | [![Build](https://github.com/sascha-egerer/phpstan-typo3/workflows/Tests/badge.svg)](https://github.com/sascha-egerer/phpstan-typo3/actions) 16 | 17 | * [PHPStan](https://phpstan.org/) 18 | 19 | This extension provides the following features (!!! not an exhaustive list !!!): 20 | 21 | **Dynamic Return Type Extensions** 22 | * Provides correct return type for `\TYPO3\CMS\Core\Context\Context->getAspect()`. 23 | * Provides correct return type for `\TYPO3\CMS\Extbase\Property\PropertyMapper->convert()`. 24 | * Provides correct return type for `\TYPO3\CMS\Core\Utility\MathUtility` methods like isIntegerInRange. 25 | * Provides correct return type for `\TYPO3\CMS\Extbase\Persistence\Generic\Query->execute()`. 26 | * Provides correct return type for `\TYPO3\CMS\Extbase\Persistence\QueryInterface->execute()`. 27 | * Provides correct return type for `\TYPO3\CMS\Core\Site\Entity\Site->getAttribute()`. 28 | * Provides correct return type for `\Psr\Http\Message\ServerRequestInterface->getAttribute()`. 29 | * Uses under the hood [bnf/phpstan-psr-container](https://github.com/bnf/phpstan-psr-container) 30 | 31 | All these dynamic return type extensions are necessary to teach PHPStan what type will be returned by the specific method call. 32 | 33 |
34 | Show me a practical use case. 35 | For example PHPStan cannot know innately what type will be returned if you call `\TYPO3\CMS\Core\Utility\MathUtility->forceIntegerInRange(1000, 1, 10)`. 36 | It will be an int<10>. With the help of this library PHPStan also knows what´s going up. 37 | 38 | Imagine the following situation in your code: 39 | 40 | ```php 41 | 42 | use TYPO3\CMS\Core\Utility\MathUtility; 43 | 44 | $integer = MathUtility::forceIntegerInRange(100, 1, 10); 45 | 46 | if($integer > 10) { 47 | throw new \UnexpectedValueException('The integer is too big') 48 | } 49 | ``` 50 | 51 | PHPStan will tell you that the if condition is superfluous, because the variable $integer will never be higher than 10. Right? 52 |
53 | 54 | **Framework specific rules** 55 | * Provides rule for `\TYPO3\CMS\Core\Context\Context->getAspect()`. 56 | * Provides rule for `\Psr\Http\Message\ServerRequestInterface->getAttribute()`. 57 | * Provides rule for `\TYPO3\CMS\Core\Site\Entity\Site->getAttribute()`. 58 | * Provides rule for `\TYPO3\CMS\Extbase\Validation\ValidatorResolver->createValidator()`. 59 | 60 |
61 | Show me a practical use case. 62 | 63 | For example PHPStan cannot know innately that calling `ValidatorResolver->createValidator(RegularExpressionValidator::class)` is invalid, because we miss to pass the required option `regularExpression`. 64 | With the help of this library PHPStan now complaints that we have missed to pass the required option. 65 | So go ahead and find bugs in your code without running it. 66 | 67 |
68 | 69 | 70 | ## Installation & Configuration 71 | 72 | To use this extension, require it in [Composer](https://getcomposer.org/): 73 | 74 | ```Shell 75 | composer require --dev saschaegerer/phpstan-typo3 76 | ``` 77 | 78 | If you also install [phpstan/extension-installer](https://github.com/phpstan/extension-installer) then you're all set! 79 | 80 |
81 | Manual installation 82 | 83 | If you don't want to use `phpstan/extension-installer`, put this into your phpstan.neon config: 84 | 85 | ```NEON 86 | includes: 87 | - vendor/saschaegerer/phpstan-typo3/extension.neon 88 | ``` 89 | 90 |
91 | 92 | ### Custom Context API Aspects 93 | 94 | If you use custom aspects for the TYPO3 Context API you can add a mapping so PHPStan knows 95 | what type of aspect class is returned by the context API 96 | 97 | ```NEON 98 | parameters: 99 | typo3: 100 | contextApiGetAspectMapping: 101 | myCustomAspect: FlowdGmbh\MyProject\Context\MyCustomAspect 102 | ``` 103 | 104 | ```PHP 105 | // PHPStan will now know that $myCustomAspect is of type FlowdGmbh\MyProject\Context\MyCustomAspect 106 | $myCustomAspect = GeneralUtility::makeInstance(Context::class)->getAspect('myCustomAspect'); 107 | ``` 108 | 109 | ### Custom Request Attribute 110 | 111 | If you use custom PSR-7 request attribute you can add a mapping so PHPStan knows 112 | what type of class is returned by Request::getAttribute() 113 | 114 | ```NEON 115 | parameters: 116 | typo3: 117 | requestGetAttributeMapping: 118 | myAttribute: FlowdGmbh\MyProject\Http\MyAttribute 119 | myNullableAttribute: FlowdGmbh\MyProject\Http\MyAttribute|null 120 | ``` 121 | 122 | ```PHP 123 | // PHPStan will now know that $myAttribute is of type FlowdGmbh\MyProject\Http\MyAttribute 124 | $myAttribute = $request->getAttribute('myAttribute'); 125 | ``` 126 | 127 | ### Custom Site Attribute 128 | 129 | If you use custom attributes for the TYPO3 Site API you can add a mapping so PHPStan knows 130 | what type is returned by the site API 131 | 132 | ```NEON 133 | parameters: 134 | typo3: 135 | siteGetAttributeMapping: 136 | myArrayAttribute: array 137 | myIntAttribute: int 138 | myStringAttribute: string 139 | ``` 140 | 141 | ```PHP 142 | $site = $this->request->getAttribute('site'); 143 | 144 | // PHPStan will now know that $myArrayAttribute is of type array 145 | $myArrayAttribute = $site->getAttribute('myArrayAttribute'); 146 | 147 | // PHPStan will now know that $myIntAttribute is of type int 148 | $myIntAttribute = $site->getAttribute('myIntAttribute'); 149 | 150 | // PHPStan will now know that $myStringAttribute is of type string 151 | $myStringAttribute = $site->getAttribute('myStringAttribute'); 152 | ``` 153 | 154 | ### Check for private Services 155 | You have to provide a path to App_KernelDevelopmentDebugContainer.xml or similar XML file describing your container. 156 | This is generated by [ssch/typo3-debug-dump-pass](https://github.com/sabbelasichon/typo3-debug-dump-pass) in your /var/cache/{TYPO3_CONTEXT}/ folder. 157 | 158 | ```NEON 159 | parameters: 160 | typo3: 161 | containerXmlPath: var/cache/development/App_KernelDevelopmentDebugContainer.xml 162 | ``` 163 | 164 | -------------------------------------------------------------------------------- /src/Type/MathUtilityTypeSpecifyingExtension.php: -------------------------------------------------------------------------------- 1 | typeSpecifier = $typeSpecifier; 39 | } 40 | 41 | public function getClass(): string 42 | { 43 | return MathUtility::class; 44 | } 45 | 46 | public function isStaticMethodSupported(MethodReflection $staticMethodReflection, StaticCall $node, TypeSpecifierContext $context): bool 47 | { 48 | return in_array( 49 | $staticMethodReflection->getName(), 50 | [ 51 | self::METHOD_FORCE_INTEGER_IN_RANGE, 52 | self::METHOD_CONVERT_TO_POSITIVE_INTEGER, 53 | self::METHOD_CAN_BE_INTERPRETED_AS_INTEGER, 54 | self::METHOD_CAN_BE_INTERPRETED_AS_FLOAT, 55 | self::METHOD_IS_INTEGER_IN_RANGE, 56 | ], 57 | true 58 | ); 59 | } 60 | 61 | public function specifyTypes(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes 62 | { 63 | if ($staticMethodReflection->getName() === self::METHOD_FORCE_INTEGER_IN_RANGE) { 64 | return $this->specifyTypesForForceIntegerInRange($node, $scope); 65 | } 66 | 67 | if ($staticMethodReflection->getName() === self::METHOD_IS_INTEGER_IN_RANGE) { 68 | return $this->specifyTypesForIsIntegerInRange($node, $scope); 69 | } 70 | 71 | if ($staticMethodReflection->getName() === self::METHOD_CONVERT_TO_POSITIVE_INTEGER) { 72 | return $this->specifyTypesForConvertToPositiveInteger($node, $scope); 73 | } 74 | 75 | if ($staticMethodReflection->getName() === self::METHOD_CAN_BE_INTERPRETED_AS_INTEGER) { 76 | return $this->specifyTypesForCanBeInterpretedAsInteger($node, $scope); 77 | } 78 | 79 | return $this->specifyTypesForCanBeInterpretedAsFloat($node, $scope); 80 | } 81 | 82 | private function specifyTypesForForceIntegerInRange(StaticCall $node, Scope $scope): SpecifiedTypes 83 | { 84 | $parentNode = $node->getAttribute('parent'); 85 | 86 | if (!$parentNode instanceof Assign) { 87 | return new SpecifiedTypes(); 88 | } 89 | 90 | $min = isset($node->getArgs()[1]) ? $node->getArgs()[1]->value : new LNumber(0); 91 | $max = isset($node->getArgs()[2]) ? $node->getArgs()[2]->value : new LNumber(2000000000); 92 | 93 | return $this->typeSpecifier->specifyTypesInCondition( 94 | $scope, 95 | new BooleanAnd( 96 | new FuncCall( 97 | new Name('is_int'), 98 | [new Arg($parentNode->var)] 99 | ), 100 | new BooleanAnd( 101 | new GreaterOrEqual( 102 | $parentNode->var, 103 | $min 104 | ), 105 | new SmallerOrEqual( 106 | $parentNode->var, 107 | $max 108 | ) 109 | ) 110 | ), 111 | TypeSpecifierContext::createTruthy() 112 | ); 113 | } 114 | 115 | private function specifyTypesForIsIntegerInRange(StaticCall $node, Scope $scope): SpecifiedTypes 116 | { 117 | $firstArgument = $node->getArgs()[0]; 118 | $firstArgumentType = $scope->getType($firstArgument->value); 119 | 120 | $min = $node->getArgs()[1]->value; 121 | $max = $node->getArgs()[2]->value; 122 | 123 | if ($firstArgumentType->isString()->no()) { 124 | $typeCheckFuncCall = new FuncCall( 125 | new Name('is_int'), 126 | [$firstArgument] 127 | ); 128 | } else { 129 | $typeCheckFuncCall = new BooleanAnd( 130 | new FuncCall( 131 | new Name('is_numeric'), 132 | [$firstArgument] 133 | ), 134 | new BooleanNot( 135 | new FuncCall( 136 | new Name('is_float'), 137 | [$firstArgument] 138 | ) 139 | ) 140 | ); 141 | } 142 | 143 | return $this->typeSpecifier->specifyTypesInCondition( 144 | $scope, 145 | new BooleanAnd( 146 | $typeCheckFuncCall, 147 | new BooleanAnd( 148 | new GreaterOrEqual( 149 | $firstArgument->value, 150 | $min 151 | ), 152 | new SmallerOrEqual( 153 | $firstArgument->value, 154 | $max 155 | ) 156 | ) 157 | ), 158 | TypeSpecifierContext::createTruthy() 159 | ); 160 | } 161 | 162 | private function specifyTypesForConvertToPositiveInteger(StaticCall $node, Scope $scope): SpecifiedTypes 163 | { 164 | $parentNode = $node->getAttribute('parent'); 165 | 166 | if (!$parentNode instanceof Assign) { 167 | return new SpecifiedTypes(); 168 | } 169 | 170 | return $this->typeSpecifier->specifyTypesInCondition( 171 | $scope, 172 | new BooleanAnd( 173 | new FuncCall( 174 | new Name('is_int'), 175 | [new Arg($parentNode)] 176 | ), 177 | new BooleanAnd( 178 | new GreaterOrEqual( 179 | $parentNode->var, 180 | new LNumber(0) 181 | ), 182 | new SmallerOrEqual( 183 | $parentNode->var, 184 | new LNumber(PHP_INT_MAX) 185 | ) 186 | ) 187 | ), 188 | TypeSpecifierContext::createTruthy() 189 | ); 190 | } 191 | 192 | private function specifyTypesForCanBeInterpretedAsInteger(StaticCall $node, Scope $scope): SpecifiedTypes 193 | { 194 | $firstArgument = $node->getArgs()[0]; 195 | 196 | return $this->typeSpecifier->specifyTypesInCondition( 197 | $scope, 198 | new FuncCall( 199 | new Name('is_numeric'), 200 | [$firstArgument] 201 | ), 202 | TypeSpecifierContext::createTruthy() 203 | ); 204 | } 205 | 206 | private function specifyTypesForCanBeInterpretedAsFloat(StaticCall $node, Scope $scope): SpecifiedTypes 207 | { 208 | $firstArgument = $node->getArgs()[0]; 209 | 210 | return $this->typeSpecifier->specifyTypesInCondition( 211 | $scope, 212 | new FuncCall( 213 | new Name('is_float'), 214 | [$firstArgument] 215 | ), 216 | TypeSpecifierContext::createTruthy() 217 | ); 218 | } 219 | 220 | } 221 | -------------------------------------------------------------------------------- /extension.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - rules.neon 3 | 4 | services: 5 | - 6 | class: PhpParser\NodeVisitor\NodeConnectingVisitor 7 | - 8 | class: SaschaEgerer\PhpstanTypo3\Type\ValidatorResolverDynamicReturnTypeExtension 9 | tags: 10 | - phpstan.broker.dynamicMethodReturnTypeExtension 11 | - 12 | class: SaschaEgerer\PhpstanTypo3\Type\ObjectStorageDynamicReturnTypeExtension 13 | tags: 14 | - phpstan.broker.dynamicMethodReturnTypeExtension 15 | - 16 | class: SaschaEgerer\PhpstanTypo3\Type\ContextDynamicReturnTypeExtension 17 | arguments: 18 | contextApiGetAspectMapping: %typo3.contextApiGetAspectMapping% 19 | tags: 20 | - phpstan.broker.dynamicMethodReturnTypeExtension 21 | - 22 | class: SaschaEgerer\PhpstanTypo3\Rule\ContextAspectValidationRule 23 | arguments: 24 | contextApiGetAspectMapping: %typo3.contextApiGetAspectMapping% 25 | tags: 26 | - phpstan.rules.rule 27 | - 28 | class: SaschaEgerer\PhpstanTypo3\Rule\RequestAttributeValidationRule 29 | arguments: 30 | requestGetAttributeMapping: %typo3.requestGetAttributeMapping% 31 | tags: 32 | - phpstan.rules.rule 33 | - 34 | class: SaschaEgerer\PhpstanTypo3\Rule\SiteAttributeValidationRule 35 | arguments: 36 | siteGetAttributeMapping: %typo3.siteGetAttributeMapping% 37 | tags: 38 | - phpstan.rules.rule 39 | - 40 | class: SaschaEgerer\PhpstanTypo3\Type\RequestDynamicReturnTypeExtension 41 | arguments: 42 | requestGetAttributeMapping: %typo3.requestGetAttributeMapping% 43 | tags: 44 | - phpstan.broker.dynamicMethodReturnTypeExtension 45 | - 46 | class: SaschaEgerer\PhpstanTypo3\Type\SiteDynamicReturnTypeExtension 47 | arguments: 48 | siteGetAttributeMapping: %typo3.siteGetAttributeMapping% 49 | tags: 50 | - phpstan.broker.dynamicMethodReturnTypeExtension 51 | - 52 | class: SaschaEgerer\PhpstanTypo3\Service\ValidatorClassNameResolver 53 | - 54 | class: SaschaEgerer\PhpstanTypo3\Type\PropertyMapperReturnTypeExtension 55 | tags: 56 | - phpstan.broker.dynamicMethodReturnTypeExtension 57 | - 58 | class: Bnf\PhpstanPsrContainer\ContainerDynamicReturnTypeExtension 59 | tags: 60 | - phpstan.broker.dynamicMethodReturnTypeExtension 61 | - 62 | class: SaschaEgerer\PhpstanTypo3\Type\MathUtilityTypeSpecifyingExtension 63 | tags: 64 | - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension 65 | - 66 | class: SaschaEgerer\PhpstanTypo3\Type\GeneralUtilityGetIndpEnvDynamicReturnTypeExtension 67 | tags: 68 | - phpstan.broker.dynamicStaticMethodReturnTypeExtension 69 | - 70 | class: SaschaEgerer\PhpstanTypo3\Type\UserAspectGetDynamicReturnTypeExtension 71 | tags: 72 | - phpstan.broker.dynamicMethodReturnTypeExtension 73 | - 74 | class: SaschaEgerer\PhpstanTypo3\Type\DateTimeAspectGetDynamicReturnTypeExtension 75 | tags: 76 | - phpstan.broker.dynamicMethodReturnTypeExtension 77 | - 78 | class: SaschaEgerer\PhpstanTypo3\Service\PrivateServiceAnalyzer 79 | - 80 | class: SaschaEgerer\PhpstanTypo3\Service\PrototypeServiceDefinitionChecker 81 | # service map 82 | typo3.serviceMapFactory: 83 | class: SaschaEgerer\PhpstanTypo3\Contract\ServiceMapFactory 84 | factory: SaschaEgerer\PhpstanTypo3\Service\XmlServiceMapFactory 85 | arguments: 86 | containerXmlPath: %typo3.containerXmlPath% 87 | - 88 | factory: @typo3.serviceMapFactory::create() 89 | parameters: 90 | bootstrapFiles: 91 | - phpstan.bootstrap.php 92 | typo3: 93 | containerXmlPath: null 94 | contextApiGetAspectMapping: 95 | backend.user: TYPO3\CMS\Core\Context\UserAspect 96 | date: TYPO3\CMS\Core\Context\DateTimeAspect 97 | fileProcessing: TYPO3\CMS\Core\Context\FileProcessingAspect 98 | frontend.preview: TYPO3\CMS\Core\Context\PreviewAspect 99 | frontend.user: TYPO3\CMS\Core\Context\UserAspect 100 | language: TYPO3\CMS\Core\Context\LanguageAspect 101 | security: TYPO3\CMS\Core\Context\SecurityAspect 102 | typoscript: TYPO3\CMS\Core\Context\TypoScriptAspect 103 | visibility: TYPO3\CMS\Core\Context\VisibilityAspect 104 | workspace: TYPO3\CMS\Core\Context\WorkspaceAspect 105 | requestGetAttributeMapping: 106 | adminPanelRequestId: string 107 | applicationType: TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_* 108 | backend.user: TYPO3\CMS\Backend\FrontendBackendUserAuthentication 109 | currentContentObject: TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer 110 | extbase: TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters 111 | frontend.cache.collector: TYPO3\CMS\Core\Cache\CacheDataCollector 112 | frontend.cache.instruction: TYPO3\CMS\Frontend\Cache\CacheInstruction 113 | frontend.controller: TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController 114 | frontend.page.information: TYPO3\CMS\Frontend\Page\PageInformation 115 | frontend.typoscript: TYPO3\CMS\Core\TypoScript\FrontendTypoScript 116 | frontend.user: TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication 117 | language: TYPO3\CMS\Core\Site\Entity\SiteLanguage 118 | module: TYPO3\CMS\Backend\Module\ModuleInterface 119 | moduleData: TYPO3\CMS\Backend\Module\ModuleData 120 | nonce: TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce 121 | normalizedParams: TYPO3\CMS\Core\Http\NormalizedParams 122 | originalRequest: Psr\Http\Message\ServerRequestInterface 123 | routing: TYPO3\CMS\Core\Routing\SiteRouteResult|TYPO3\CMS\Core\Routing\PageArguments 124 | site: TYPO3\CMS\Core\Site\Entity\Site 125 | target: string 126 | typo3.testing.context: TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext 127 | siteGetAttributeMapping: 128 | base: string 129 | baseVariants: list 130 | errorHandling: list 131 | languages: list 132 | rootPageId: int 133 | routeEnhancers: array 134 | settings: array 135 | websiteTitle: string 136 | stubFiles: 137 | - stubs/DomainObjectInterface.stub 138 | - stubs/ObjectStorage.stub 139 | - stubs/QueryInterface.stub 140 | - stubs/QueryResultInterface.stub 141 | - stubs/Repository.stub 142 | - stubs/RepositoryInterface.stub 143 | dynamicConstantNames: 144 | - TYPO3_MODE 145 | - TYPO3_REQUESTTYPE 146 | - TYPO3_COMPOSER_MODE 147 | - TYPO3_branch 148 | - TYPO3_version 149 | - TYPO3_OS 150 | - TYPO3_copyright_year 151 | - PATH_thisScript 152 | - PATH_site 153 | - PATH_typo3conf 154 | - PATH_typo3 155 | - TYPO3_mainDir 156 | earlyTerminatingMethodCalls: 157 | TYPO3\CMS\Extbase\Mvc\Controller\ActionController: 158 | - redirectToUri 159 | - redirect 160 | - forward 161 | - forwardToReferringRequest 162 | - throwStatus 163 | TYPO3\CMS\Extbase\Mvc\Controller\AbstractController: 164 | - redirectToUri 165 | - redirect 166 | - forward 167 | - throwStatus 168 | TYPO3\CMS\Core\Utility\HttpUtility: 169 | - redirect 170 | - setResponseCodeAndExit 171 | TYPO3\CMS\Form\Domain\Finishers\RedirectFinisher: 172 | - redirectToUri 173 | TYPO3\CMS\Extbase\Mvc\Controller\CommandController: 174 | - forward 175 | - quit 176 | TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility: 177 | - sendZipFileToBrowserAndDelete 178 | TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController: 179 | - pageUnavailableAndExit 180 | - pageNotFoundAndExit 181 | - pageErrorHandler 182 | TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList: 183 | - pageErrorHandler 184 | parametersSchema: 185 | typo3: structure([ 186 | containerXmlPath: schema(string(), nullable()) 187 | contextApiGetAspectMapping: arrayOf(string()) 188 | requestGetAttributeMapping: arrayOf(string()) 189 | siteGetAttributeMapping: arrayOf(string()) 190 | ]) 191 | conditionalTags: 192 | PhpParser\NodeVisitor\NodeConnectingVisitor: 193 | phpstan.parser.richParserNodeVisitor: true 194 | -------------------------------------------------------------------------------- /src/Rule/ValidatorResolverOptionsRule.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | final class ValidatorResolverOptionsRule implements Rule 31 | { 32 | 33 | private InitializerExprTypeResolver $initializerExprTypeResolver; 34 | 35 | private ValidatorClassNameResolver $validatorClassNameResolver; 36 | 37 | public function __construct( 38 | InitializerExprTypeResolver $initializerExprTypeResolver, 39 | ValidatorClassNameResolver $validatorClassNameResolver 40 | ) 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) { 72 | if ($validatorType->getConstantStrings() !== []) { 73 | $validatorClassName = $validatorType->getConstantStrings()[0]->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) 81 | ->identifier('phpstanTypo3.validatorResolverOptions.noSuchValidator') 82 | ->build(), 83 | ]; 84 | } 85 | 86 | if ($validatorClassName === null) { 87 | return []; 88 | } 89 | 90 | $validatorObjectType = new ObjectType($validatorClassName); 91 | $validatorClassReflection = $validatorObjectType->getClassReflection(); 92 | 93 | if (!$validatorClassReflection instanceof ClassReflection) { 94 | return []; 95 | } 96 | 97 | if (!$validatorClassReflection->isSubclassOf(AbstractValidator::class)) { 98 | return []; 99 | } 100 | 101 | try { 102 | $supportedOptions = $validatorClassReflection->getProperty('supportedOptions', $scope); 103 | } catch (\PHPStan\Reflection\MissingPropertyFromReflectionException $e) { 104 | return []; 105 | } 106 | 107 | $validatorOptionsConfiguration = $this->extractValidatorOptionsConfiguration($supportedOptions, $scope); 108 | $providedOptionsArray = $this->extractProvidedOptions($validatorOptionsArgument, $scope); 109 | 110 | $unsupportedOptions = array_diff($providedOptionsArray, $validatorOptionsConfiguration->getSupportedOptions()); 111 | $neededRequiredOptions 112 | = array_diff($validatorOptionsConfiguration->getRequiredOptions(), $providedOptionsArray); 113 | 114 | $errors = []; 115 | 116 | if ($neededRequiredOptions !== []) { 117 | foreach ($neededRequiredOptions as $neededRequiredOption) { 118 | $errorMessage = sprintf('Required validation option not set: %s', $neededRequiredOption); 119 | $errors[] = RuleErrorBuilder::message($errorMessage) 120 | ->identifier('phpstanTypo3.validatorResolverOptions.requiredValidatorOptionNotSet') 121 | ->build(); 122 | } 123 | } 124 | 125 | if ($unsupportedOptions !== []) { 126 | $errorMessage = 'Unsupported validation option(s) found: ' . implode(', ', $unsupportedOptions); 127 | $errors[] = RuleErrorBuilder::message($errorMessage) 128 | ->identifier('phpstanTypo3.validatorResolverOptions.unsupportedValidationOption') 129 | ->build(); 130 | } 131 | 132 | return $errors; 133 | } 134 | 135 | private function shouldSkip(MethodCall $methodCall, Scope $scope): bool 136 | { 137 | $objectType = $scope->getType($methodCall->var); 138 | $validatorResolverType = new ObjectType(ValidatorResolver::class); 139 | 140 | if ($validatorResolverType->isSuperTypeOf($objectType)->no()) { 141 | return true; 142 | } 143 | 144 | if (!$methodCall->name instanceof Identifier) { 145 | return true; 146 | } 147 | 148 | return $methodCall->name->toString() !== 'createValidator'; 149 | } 150 | 151 | /** 152 | * @return string[] 153 | */ 154 | private function extractProvidedOptions(?Arg $validatorOptionsArgument, Scope $scope): array 155 | { 156 | if (!$validatorOptionsArgument instanceof Arg) { 157 | return []; 158 | } 159 | 160 | $providedOptionsArray = []; 161 | 162 | $validatorOptionsArgumentType = $scope->getType($validatorOptionsArgument->value); 163 | 164 | if ($validatorOptionsArgumentType->getConstantArrays() === []) { 165 | return []; 166 | } 167 | 168 | $keysArray = $validatorOptionsArgumentType->getConstantArrays()[0]->getKeyTypes(); 169 | 170 | foreach ($keysArray as $valueType) { 171 | $providedOptionsArray[] = (string) $valueType->getValue(); 172 | } 173 | 174 | return $providedOptionsArray; 175 | } 176 | 177 | private function extractValidatorOptionsConfiguration( 178 | PropertyReflection $supportedOptions, 179 | Scope $scope 180 | ): ValidatorOptionsConfiguration 181 | { 182 | $collectedSupportedOptions = []; 183 | $collectedRequiredOptions = []; 184 | 185 | if (!$supportedOptions instanceof PhpPropertyReflection) { 186 | return ValidatorOptionsConfiguration::empty(); 187 | } 188 | 189 | $defaultValues = $supportedOptions->getNativeReflection()->getDefaultValueExpression(); 190 | 191 | if (!$defaultValues instanceof Array_) { 192 | return ValidatorOptionsConfiguration::empty(); 193 | } 194 | 195 | foreach ($defaultValues->items as $defaultValue) { 196 | 197 | if ($defaultValue->key === null) { 198 | continue; 199 | } 200 | 201 | $supportedOptionKey = $this->resolveOptionKeyValue($defaultValue, $supportedOptions, $scope); 202 | 203 | if ($supportedOptionKey === null) { 204 | continue; 205 | } 206 | 207 | $collectedSupportedOptions[] = $supportedOptionKey; 208 | 209 | $optionDefinition = $defaultValue->value; 210 | if (!$optionDefinition instanceof Array_) { 211 | continue; 212 | } 213 | 214 | if (!isset($optionDefinition->items[3])) { 215 | continue; 216 | } 217 | 218 | $requiredValueType = $scope->getType($optionDefinition->items[3]->value); 219 | 220 | if ($requiredValueType->isBoolean()->no()) { 221 | continue; 222 | } 223 | 224 | if ($requiredValueType->isFalse()->yes()) { 225 | continue; 226 | } 227 | 228 | $collectedRequiredOptions[] = $supportedOptionKey; 229 | } 230 | 231 | return new ValidatorOptionsConfiguration($collectedSupportedOptions, $collectedRequiredOptions); 232 | } 233 | 234 | private function resolveOptionKeyValue( 235 | ArrayItem $defaultValue, 236 | PhpPropertyReflection $supportedOptions, 237 | Scope $scope 238 | ): ?string 239 | { 240 | if ($defaultValue->key === null) { 241 | return null; 242 | } 243 | 244 | if ($defaultValue->key instanceof ClassConstFetch && $defaultValue->key->name instanceof Identifier) { 245 | $keyType = $this->initializerExprTypeResolver->getClassConstFetchType( 246 | $defaultValue->key->class, 247 | $defaultValue->key->name->toString(), 248 | $supportedOptions->getDeclaringClass()->getName(), 249 | static function (Expr $expr) use ($scope): Type { 250 | return $scope->getType($expr); 251 | } 252 | ); 253 | 254 | if ($keyType->getConstantStrings() !== []) { 255 | return $keyType->getConstantStrings()[0]->getValue(); 256 | } 257 | 258 | return null; 259 | } 260 | 261 | $keyType = $scope->getType($defaultValue->key); 262 | 263 | if ($keyType->getConstantStrings() !== []) { 264 | return $keyType->getConstantStrings()[0]->getValue(); 265 | } 266 | 267 | return null; 268 | } 269 | 270 | } 271 | --------------------------------------------------------------------------------