├── src ├── Stubs │ ├── base │ │ ├── Event.stub │ │ ├── Request.stub │ │ ├── Response.stub │ │ ├── BaseObject.stub │ │ ├── Component.stub │ │ ├── ActionEvent.stub │ │ ├── Behavior.stub │ │ ├── DynamicModel.stub │ │ ├── Action.stub │ │ ├── Module.stub │ │ ├── InlineAction.stub │ │ ├── Model.stub │ │ └── Controller.stub │ ├── db │ │ ├── Command.stub │ │ ├── ActiveRecord.stub │ │ ├── BatchQueryResult.stub │ │ ├── Connection.stub │ │ ├── SqlTokenizer.stub │ │ ├── DataReader.stub │ │ ├── QueryBuilder.stub │ │ ├── Expression.stub │ │ ├── ColumnSchemaBuilder.stub │ │ ├── Migration.stub │ │ └── ActiveQuery.stub │ ├── web │ │ ├── Cookie.stub │ │ ├── JsExpression.stub │ │ ├── HeaderCollection.stub │ │ └── CookieCollection.stub │ ├── validators │ │ ├── InlineValidator.stub │ │ └── Validator.stub │ ├── test │ │ ├── Fixture.stub │ │ └── BaseActiveFixture.stub │ ├── data │ │ ├── ActiveDataProvider.stub │ │ ├── DataProviderInterface.stub │ │ ├── SqlDataProvider.stub │ │ ├── ArrayDataProvider.stub │ │ └── BaseDataProvider.stub │ └── BaseYii.stub ├── Type │ ├── ActiveQueryObjectType.php │ ├── ContainerDynamicMethodReturnTypeExtension.php │ ├── ActiveRecordObjectType.php │ ├── ActiveRecordRelationReturnTypeExtension.php │ ├── ActiveRecordRelationGetterReturnTypeExtension.php │ ├── ActiveRecordFindReturnTypeExtension.php │ └── ActiveQueryBuilderReturnTypeExtension.php ├── Reflection │ ├── RequestMethodsClassReflectionExtension.php │ ├── RequestPropertiesClassReflectionExtension.php │ ├── ResponsePropertiesClassReflectionExtension.php │ ├── UserPropertiesClassReflectionExtension.php │ ├── ComponentPropertyReflection.php │ ├── ApplicationPropertiesClassReflectionExtension.php │ ├── BaseObjectPropertiesClassReflectionExtension.php │ └── BaseObjectPropertyReflection.php ├── Rule │ ├── CreateConfigurableObjectRule.php │ ├── CreateObjectRule.php │ └── YiiConfigHelper.php └── ServiceMap.php ├── rules.neon ├── CHANGELOG.md ├── .php-cs-fixer.dist.php ├── phpstan.neon.dist ├── LICENSE ├── composer.json ├── README.md └── extension.neon /src/Stubs/base/Event.stub: -------------------------------------------------------------------------------- 1 | > 10 | */ 11 | public $depends = []; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/Stubs/test/BaseActiveFixture.stub: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | public $modelClass; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/Stubs/base/BaseObject.stub: -------------------------------------------------------------------------------- 1 | $config 10 | */ 11 | public function __construct($config = []) {} 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/Stubs/db/BatchQueryResult.stub: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class BatchQueryResult implements \Iterator { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/Stubs/db/Connection.stub: -------------------------------------------------------------------------------- 1 | }|null 11 | */ 12 | protected function validateValue($value) {} 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/Stubs/base/Component.stub: -------------------------------------------------------------------------------- 1 | |array{class: class-string}|array{__class: class-string}|callable(): Behavior> 10 | */ 11 | public function behaviors() {} 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/Stubs/db/SqlTokenizer.stub: -------------------------------------------------------------------------------- 1 | $config 13 | */ 14 | public function __construct($sql, $config = []) {} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Stubs/web/JsExpression.stub: -------------------------------------------------------------------------------- 1 | $config 13 | */ 14 | public function __construct($expression, $config = []) {} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Stubs/base/ActionEvent.stub: -------------------------------------------------------------------------------- 1 | $config 13 | */ 14 | public function __construct($action, $config = []) {} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Stubs/base/Behavior.stub: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | public function events() {} 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/Stubs/db/DataReader.stub: -------------------------------------------------------------------------------- 1 | $config 13 | */ 14 | public function __construct(Command $command, $config = []) {} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Stubs/db/QueryBuilder.stub: -------------------------------------------------------------------------------- 1 | $config 13 | */ 14 | public function __construct($connection, $config = []) {} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Stubs/web/HeaderCollection.stub: -------------------------------------------------------------------------------- 1 | |T) 14 | */ 15 | public function get($name, $default = null, $first = true) {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/Stubs/data/ActiveDataProvider.stub: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class ActiveDataProvider extends BaseDataProvider { 12 | 13 | // TODO: make $query field generic 14 | 15 | /** 16 | * @var string|callable(TValue $row): TKey|null 17 | */ 18 | public $key; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## Unreleased 8 | ### Added 9 | - Initial release. This project was based on work of Arkadiusz Kondas and Marcin Michalski: https://github.com/proget-hq/phpstan-yii2. 10 | -------------------------------------------------------------------------------- /src/Stubs/base/DynamicModel.stub: -------------------------------------------------------------------------------- 1 | $attributes 12 | * @param array $config 13 | */ 14 | public function __construct(array $attributes = [], $config = []) {} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Stubs/data/DataProviderInterface.stub: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function getModels(); 16 | 17 | /** 18 | * @return list 19 | */ 20 | public function getKeys(); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/Stubs/web/CookieCollection.stub: -------------------------------------------------------------------------------- 1 | $cookies 12 | * @param array $config 13 | */ 14 | public function __construct($cookies = [], $config = []) {} 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Stubs/base/Action.stub: -------------------------------------------------------------------------------- 1 | $config 14 | */ 15 | public function __construct($id, $controller, $config = []) {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/Stubs/base/Module.stub: -------------------------------------------------------------------------------- 1 | $config 14 | */ 15 | public function __construct($id, $parent = null, $config = []) {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/Stubs/db/Expression.stub: -------------------------------------------------------------------------------- 1 | $params 13 | * @param array $config 14 | */ 15 | public function __construct($expression, $params = [], $config = []) {} 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/Stubs/data/SqlDataProvider.stub: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class SqlDataProvider extends BaseDataProvider { 12 | 13 | /** 14 | * @var array 15 | */ 16 | public $params; 17 | 18 | /** 19 | * @var string|callable(TValue $row): TKey|null 20 | */ 21 | public $key; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/Stubs/base/InlineAction.stub: -------------------------------------------------------------------------------- 1 | $config 15 | */ 16 | public function __construct($id, $controller, $actionMethod, $config = []) {} 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/Stubs/db/ColumnSchemaBuilder.stub: -------------------------------------------------------------------------------- 1 | |null $length 13 | * @param \yii\db\Connection|null $db 14 | * @param array $config 15 | */ 16 | public function __construct($type, $length = null, $db = null, $config = []) {} 17 | 18 | } 19 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in(__DIR__); 6 | 7 | return Ely\CS\Config::create([ 8 | // Disable "parameters" and "match" to keep compatibility with PHP 7.4 9 | 'trailing_comma_in_multiline' => [ 10 | 'elements' => ['arrays', 'arguments'], 11 | ], 12 | // Add some really strict rules 13 | 'declare_strict_types' => true, 14 | 'final_class' => true, 15 | // Disable controversial rules 16 | 'comment_to_phpdoc' => false, 17 | ])->setFinder($finder); 18 | -------------------------------------------------------------------------------- /src/Stubs/data/ArrayDataProvider.stub: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class ArrayDataProvider extends BaseDataProvider { 12 | 13 | /** 14 | * @var list 15 | */ 16 | public $allModels; 17 | 18 | /** 19 | * @var string|callable(TValue $row): TKey|null 20 | */ 21 | public $key; 22 | 23 | /** 24 | * @var class-string<\yii\base\Model> 25 | */ 26 | public $modelClass; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/Stubs/db/Migration.stub: -------------------------------------------------------------------------------- 1 | |callable(string $attribute, array $params, \yii\validators\InlineValidator $validator, mixed $value): void}> 10 | */ 11 | public function rules() {} 12 | 13 | /** 14 | * @param string|null $attribute 15 | * @return ($attribute is null ? array : list) 16 | */ 17 | public function getErrors($attribute = null) {} 18 | 19 | /** 20 | * @return array 21 | */ 22 | public function getFirstErrors() {} 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Stubs/base/Controller.stub: -------------------------------------------------------------------------------- 1 | $config 18 | */ 19 | public function __construct($id, $module, $config = []) {} 20 | 21 | /** 22 | * @return array|array{class: class-string}|array{__class: class-string}|callable(): Action> 23 | */ 24 | public function actions() {} 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/Stubs/data/BaseDataProvider.stub: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | abstract class BaseDataProvider implements DataProviderInterface { 12 | 13 | /** 14 | * @return list 15 | */ 16 | abstract protected function prepareModels() {} 17 | 18 | /** 19 | * @param list $models 20 | * @return list 21 | */ 22 | abstract protected function prepareKeys($models) {} 23 | 24 | /** 25 | * @param list $models 26 | * @return void 27 | */ 28 | public function setModels($models) {} 29 | 30 | /** 31 | * @param list $keys 32 | * @return void 33 | */ 34 | public function setKeys($keys) {} 35 | 36 | } 37 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - phar://phpstan.phar/conf/bleedingEdge.neon 3 | - extension.neon 4 | - rules.neon 5 | 6 | parameters: 7 | bootstrapFiles: 8 | - vendor/yiisoft/yii2/Yii.php 9 | paths: 10 | - src 11 | - tests 12 | excludePaths: 13 | - tests/*/_data/* 14 | level: max 15 | yii2: 16 | config_path: tests/assets/yii-config-valid.php 17 | ignoreErrors: 18 | - '#Calling PHPStan\\Reflection\\Annotations\\AnnotationsPropertiesClassReflectionExtension\:\:(has|get)Property\(\) is not covered.+#' 19 | - '#Creating new PHPStan\\Reflection\\Dummy\\DummyPropertyReflection is not covered.+#' 20 | # PHPStan 2.1.13 introduced a constructor with the $name parameter 21 | - message: '#Class PHPStan\\Reflection\\Dummy\\DummyPropertyReflection does not have a constructor and must be instantiated without any parameters\.#' 22 | reportUnmatched: false 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 ErickSkrauch 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/Type/ActiveQueryObjectType.php: -------------------------------------------------------------------------------- 1 | returnType = $returnType; 26 | $this->asArray = $asArray; 27 | $this->hasIndexBy = $hasIndexBy; 28 | } 29 | 30 | public function getReturnType(): Type { 31 | return $this->returnType; 32 | } 33 | 34 | public function isAsArray(): bool { 35 | return $this->asArray; 36 | } 37 | 38 | public function hasIndexBy(): bool { 39 | return $this->hasIndexBy; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/Reflection/RequestMethodsClassReflectionExtension.php: -------------------------------------------------------------------------------- 1 | reflectionProvider = $reflectionProvider; 19 | } 20 | 21 | public function hasMethod(ClassReflection $classReflection, string $methodName): bool { 22 | if ($classReflection->getName() !== ConsoleRequest::class) { 23 | return false; 24 | } 25 | 26 | return $this->reflectionProvider->getClass(WebRequest::class)->hasMethod($methodName); 27 | } 28 | 29 | public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection { 30 | return $this->reflectionProvider->getClass(WebRequest::class)->getNativeMethod($methodName); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Stubs/db/ActiveQuery.stub: -------------------------------------------------------------------------------- 1 | $modelClass 13 | * @param array $config 14 | */ 15 | public function __construct($modelClass, $config = []) {} 16 | 17 | /** 18 | * @param \yii\db\Connection|null $db 19 | * @return T|array|null 20 | */ 21 | public function one($db = null) {} 22 | 23 | /** 24 | * @param \yii\db\Connection|null $db 25 | * @return T[]|array 26 | */ 27 | public function all($db = null) {} 28 | 29 | /** 30 | * @param string|callable(mixed $row): string|null $column 31 | * @return $this 32 | */ 33 | public function indexBy($column) {} 34 | 35 | /** 36 | * @param int $batchSize 37 | * @param \yii\db\Connection|null $db 38 | * @return \yii\db\BatchQueryResult>> 39 | */ 40 | public function batch($batchSize = 100, $db = null) {} 41 | 42 | /** 43 | * @param int $batchSize 44 | * @param \yii\db\Connection|null $db 45 | * @return \yii\db\BatchQueryResult> 46 | */ 47 | public function each($batchSize = 100, $db = null) {} 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Reflection/RequestPropertiesClassReflectionExtension.php: -------------------------------------------------------------------------------- 1 | reflectionProvider = $reflectionProvider; 20 | } 21 | 22 | public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { 23 | if ($classReflection->getName() !== ConsoleRequest::class) { 24 | return false; 25 | } 26 | 27 | return $this->reflectionProvider->getClass(WebRequest::class)->hasProperty($propertyName); 28 | } 29 | 30 | public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection { 31 | return $this->reflectionProvider->getClass(WebRequest::class)->getProperty($propertyName, new OutOfClassScope()); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Reflection/ResponsePropertiesClassReflectionExtension.php: -------------------------------------------------------------------------------- 1 | reflectionProvider = $reflectionProvider; 20 | } 21 | 22 | public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { 23 | if ($classReflection->getName() !== ConsoleResponse::class) { 24 | return false; 25 | } 26 | 27 | return $this->reflectionProvider->getClass(WebResponse::class)->hasProperty($propertyName); 28 | } 29 | 30 | public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection { 31 | return $this->reflectionProvider->getClass(WebResponse::class)->getProperty($propertyName, new OutOfClassScope()); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Stubs/BaseYii.stub: -------------------------------------------------------------------------------- 1 | |array{class: class-string}|array{__class: class-string}|callable(): T $type 11 | * @param array $params 12 | * 13 | * @return T 14 | */ 15 | public static function createObject($type, array $params = []) {} 16 | 17 | /** 18 | * @param string|array|\Throwable $message 19 | * @param string $category 20 | * @return void 21 | */ 22 | public static function debug($message, $category = 'application') {} 23 | 24 | /** 25 | * @param string|array|\Throwable $message 26 | * @param string $category 27 | * @return void 28 | */ 29 | public static function trace($message, $category = 'application') {} 30 | 31 | /** 32 | * @param string|array|\Throwable $message 33 | * @param string $category 34 | * @return void 35 | */ 36 | public static function error($message, $category = 'application') {} 37 | 38 | /** 39 | * @param string|array|\Throwable $message 40 | * @param string $category 41 | * @return void 42 | */ 43 | public static function warning($message, $category = 'application') {} 44 | 45 | /** 46 | * @param string|array|\Throwable $message 47 | * @param string $category 48 | * @return void 49 | */ 50 | public static function info($message, $category = 'application') {} 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/Type/ContainerDynamicMethodReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | serviceMap = $serviceMap; 23 | } 24 | 25 | public function getClass(): string { 26 | return Container::class; 27 | } 28 | 29 | public function isMethodSupported(MethodReflection $methodReflection): bool { 30 | return $methodReflection->getName() === 'get'; 31 | } 32 | 33 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { 34 | if (isset($methodCall->args[0]) && $methodCall->args[0] instanceof Arg) { 35 | $serviceClass = $this->serviceMap->getServiceClassFromNode($methodCall->args[0]->value); 36 | if ($serviceClass !== null) { 37 | return new ObjectType($serviceClass); 38 | } 39 | } 40 | 41 | return ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getReturnType(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "erickskrauch/phpstan-yii2", 3 | "description": "Yii2 extension for PHPStan", 4 | "license": "MIT", 5 | "type": "library", 6 | "authors": [ 7 | { 8 | "name": "ErickSkrauch", 9 | "email": "erickskrauch@ely.by" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.4 || ^8.0", 14 | "nikic/php-parser": "^4 || ^5", 15 | "phpstan/phpstan": "^2", 16 | "yiisoft/yii2": "~2.0.36" 17 | }, 18 | "require-dev": { 19 | "ely/php-code-style": "^1", 20 | "ergebnis/composer-normalize": "^2.28", 21 | "friendsofphp/php-cs-fixer": "^3.13", 22 | "phpstan/extension-installer": "^1.1", 23 | "phpstan/phpstan-phpunit": "^2", 24 | "phpunit/phpunit": "^9" 25 | }, 26 | "repositories": [ 27 | { 28 | "type": "composer", 29 | "url": "https://asset-packagist.org" 30 | } 31 | ], 32 | "autoload": { 33 | "psr-4": { 34 | "ErickSkrauch\\PHPStan\\Yii2\\": "src/" 35 | }, 36 | "exclude-from-classmap": [ 37 | "src/Stubs/*" 38 | ] 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "ErickSkrauch\\PHPStan\\Yii2\\Tests\\": "tests/" 43 | } 44 | }, 45 | "config": { 46 | "allow-plugins": { 47 | "ergebnis/composer-normalize": true, 48 | "phpstan/extension-installer": true, 49 | "yiisoft/yii2-composer": true 50 | }, 51 | "sort-packages": true 52 | }, 53 | "extra": { 54 | "phpstan": { 55 | "includes": [ 56 | "extension.neon", 57 | "rules.neon" 58 | ] 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Reflection/UserPropertiesClassReflectionExtension.php: -------------------------------------------------------------------------------- 1 | annotationsProperties = $annotationsProperties; 20 | } 21 | 22 | public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { 23 | if ($classReflection->getName() !== User::class) { 24 | return false; 25 | } 26 | 27 | return $classReflection->hasNativeProperty($propertyName) 28 | || $this->annotationsProperties->hasProperty($classReflection, $propertyName); 29 | } 30 | 31 | public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection { 32 | if ($propertyName === 'identity') { 33 | return new ComponentPropertyReflection(new DummyPropertyReflection($propertyName), new MixedType()); 34 | } 35 | 36 | if ($classReflection->hasNativeProperty($propertyName)) { 37 | return $classReflection->getNativeProperty($propertyName); 38 | } 39 | 40 | return $this->annotationsProperties->getProperty($classReflection, $propertyName); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/Type/ActiveRecordObjectType.php: -------------------------------------------------------------------------------- 1 | getConstantStrings(); 20 | if (empty($constantStrings)) { 21 | return TrinaryLogic::createNo(); 22 | } 23 | 24 | if ($this->isInstanceOf(ArrayAccess::class)->yes()) { 25 | return TrinaryLogic::lazyExtremeIdentity($constantStrings, function(ConstantStringType $offset): TrinaryLogic { 26 | return $this->hasProperty($offset->getValue()); 27 | }); 28 | } 29 | 30 | return parent::hasOffsetValueType($offsetType); 31 | } 32 | 33 | public function getOffsetValueType(Type $offsetType): Type { 34 | $constantStrings = $offsetType->getConstantStrings(); 35 | if (empty($constantStrings)) { 36 | throw new ShouldNotHappenException(); 37 | } 38 | 39 | $types = []; 40 | foreach ($constantStrings as $offset) { 41 | $types[] = $this->getProperty($offset->getValue(), new OutOfClassScope())->getReadableType(); 42 | } 43 | 44 | return TypeCombinator::union(...$types); 45 | } 46 | 47 | public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { 48 | if ($offsetType === null) { 49 | return new ErrorType(); 50 | } 51 | 52 | $constantStrings = $offsetType->getConstantStrings(); 53 | if (empty($constantStrings)) { 54 | throw new ShouldNotHappenException(); 55 | } 56 | 57 | $types = []; 58 | foreach ($constantStrings as $offset) { 59 | $types[] = $this->getProperty($offset->getValue(), new OutOfClassScope())->getWritableType(); 60 | } 61 | 62 | return TypeCombinator::union(...$types); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/Reflection/ComponentPropertyReflection.php: -------------------------------------------------------------------------------- 1 | fallbackProperty = $fallbackProperty; 19 | $this->type = $type; 20 | } 21 | 22 | // TODO: seems to be unused 23 | public function getType(): Type { 24 | return $this->type; 25 | } 26 | 27 | public function isReadable(): bool { 28 | return $this->fallbackProperty->isReadable(); 29 | } 30 | 31 | public function isWritable(): bool { 32 | return $this->fallbackProperty->isWritable(); 33 | } 34 | 35 | public function getDeclaringClass(): ClassReflection { 36 | return $this->fallbackProperty->getDeclaringClass(); 37 | } 38 | 39 | public function isStatic(): bool { 40 | return $this->fallbackProperty->isStatic(); 41 | } 42 | 43 | public function isPrivate(): bool { 44 | return $this->fallbackProperty->isPrivate(); 45 | } 46 | 47 | public function isPublic(): bool { 48 | return $this->fallbackProperty->isPublic(); 49 | } 50 | 51 | public function getReadableType(): Type { 52 | return $this->fallbackProperty->getReadableType(); 53 | } 54 | 55 | public function getWritableType(): Type { 56 | return $this->fallbackProperty->getWritableType(); 57 | } 58 | 59 | public function canChangeTypeAfterAssignment(): bool { 60 | return $this->fallbackProperty->canChangeTypeAfterAssignment(); 61 | } 62 | 63 | public function isDeprecated(): TrinaryLogic { 64 | return $this->fallbackProperty->isDeprecated(); 65 | } 66 | 67 | public function getDeprecatedDescription(): ?string { 68 | return $this->fallbackProperty->getDeprecatedDescription(); 69 | } 70 | 71 | public function isInternal(): TrinaryLogic { 72 | return $this->fallbackProperty->isInternal(); 73 | } 74 | 75 | public function getDocComment(): ?string { 76 | return $this->fallbackProperty->getDocComment(); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Type/ActiveRecordRelationReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | reflectionProvider = $reflectionProvider; 24 | } 25 | 26 | public function getClass(): string { 27 | return BaseActiveRecord::class; 28 | } 29 | 30 | public function isMethodSupported(MethodReflection $methodReflection): bool { 31 | return in_array($methodReflection->getName(), ['hasOne', 'hasMany'], true); 32 | } 33 | 34 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { 35 | // When method call is invalid - do nothing 36 | if (!isset($methodCall->args[0]) || !$methodCall->args[0] instanceof Arg) { 37 | return null; 38 | } 39 | 40 | $argType = $scope->getType($methodCall->args[0]->value); 41 | if (!$argType->isClassString()->yes()) { 42 | throw new ShouldNotHappenException(sprintf('Invalid argument provided to method %s' . PHP_EOL . 'Hint: You should use ::class instead of ::className()', $methodReflection->getName())); 43 | } 44 | 45 | $types = []; 46 | foreach ($argType->getConstantStrings() as $constantString) { 47 | $class = $this->reflectionProvider->getClass($constantString->getValue()); 48 | $type = ParametersAcceptorSelector::combineAcceptors($class->getMethod('find', $scope)->getVariants())->getReturnType(); 49 | if (!$type->isObject()->yes()) { 50 | throw new ShouldNotHappenException(sprintf('Return type of %s::%s must be an object', $class->getName(), $methodReflection->getName())); 51 | } 52 | 53 | foreach ($type->getObjectClassNames() as $className) { 54 | $types[] = new ActiveQueryObjectType(new ActiveRecordObjectType($class->getName()), $className); 55 | } 56 | } 57 | 58 | return TypeCombinator::union(...$types); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/Reflection/ApplicationPropertiesClassReflectionExtension.php: -------------------------------------------------------------------------------- 1 | annotationsProperties = $annotationsProperties; 31 | $this->serviceMap = $serviceMap; 32 | $this->reflectionProvider = $reflectionProvider; 33 | } 34 | 35 | public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { 36 | if ($classReflection->getName() !== BaseApplication::class && !$classReflection->isSubclassOf(BaseApplication::class)) { 37 | return false; 38 | } 39 | 40 | if ($classReflection->getName() !== WebApplication::class) { 41 | $classReflection = $this->reflectionProvider->getClass(WebApplication::class); 42 | } 43 | 44 | return $classReflection->hasNativeProperty($propertyName) 45 | || $this->annotationsProperties->hasProperty($classReflection, $propertyName) 46 | || $this->serviceMap->getComponentClassById($propertyName); 47 | } 48 | 49 | public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection { 50 | if ($classReflection->getName() !== WebApplication::class) { 51 | $classReflection = $this->reflectionProvider->getClass(WebApplication::class); 52 | } 53 | 54 | $componentClass = $this->serviceMap->getComponentClassById($propertyName); 55 | if ($componentClass !== null) { 56 | return new ComponentPropertyReflection(new DummyPropertyReflection($propertyName), new ObjectType($componentClass)); 57 | } 58 | 59 | if ($classReflection->hasNativeProperty($propertyName)) { 60 | return $classReflection->getNativeProperty($propertyName); 61 | } 62 | 63 | return $this->annotationsProperties->getProperty($classReflection, $propertyName); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/Type/ActiveRecordRelationGetterReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | reflectionProvider = $reflectionProvider; 24 | } 25 | 26 | public function getClass(): string { 27 | return BaseActiveRecord::class; 28 | } 29 | 30 | public function isMethodSupported(MethodReflection $methodReflection): bool { 31 | if (!str_starts_with($methodReflection->getName(), 'get')) { 32 | return false; 33 | } 34 | 35 | $returnType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getReturnType(); 36 | if (!$returnType->isObject()->yes()) { 37 | return false; 38 | } 39 | 40 | foreach ($returnType->getObjectClassNames() as $className) { 41 | if (!$this->reflectionProvider->getClass($className)->is(ActiveQuery::class)) { 42 | return false; 43 | } 44 | } 45 | 46 | return true; 47 | } 48 | 49 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { 50 | $returnType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getReturnType(); 51 | if ($returnType instanceof ActiveQueryObjectType) { 52 | return $returnType; 53 | } 54 | 55 | if (!$returnType->isObject()->yes()) { 56 | throw new ShouldNotHappenException(sprintf('Unexpected type %s during method call %s at line %d', get_class($returnType), $methodReflection->getName(), $methodCall->getLine())); 57 | } 58 | 59 | $arType = $returnType->getTemplateType(ActiveQuery::class, 'T'); 60 | if (!$arType->isObject()->yes()) { 61 | throw new ShouldNotHappenException(sprintf('Unexpected type %s during method call %s at line %d', get_class($arType), $methodReflection->getName(), $methodCall->getLine())); 62 | } 63 | 64 | $types = []; 65 | foreach ($arType->getObjectClassNames() as $arClassName) { 66 | foreach ($returnType->getObjectClassNames() as $className) { 67 | $types[] = new ActiveQueryObjectType(new ActiveRecordObjectType($arClassName), $className); 68 | } 69 | } 70 | 71 | return TypeCombinator::union(...$types); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yii2 extension for PHPStan 2 | 3 | An extension for [PHPStan](https://phpstan.org) providing types support and rules to work with the [Yii2 framework](https://www.yiiframework.com). Hardfork of [proget-hq/phpstan-yii2](https://github.com/proget-hq/phpstan-yii2). 4 | 5 | [![Latest Version on Packagist][ico-version]][link-packagist] 6 | [![Total Downloads][ico-downloads]][link-downloads] 7 | [![Software License][ico-license]](LICENSE.md) 8 | [![Build Status][ico-build-status]][link-build-status] 9 | 10 | ## What does it do? 11 | 12 | * Provides stub files for better analysis of array shapes. 13 | * Provide array shape analyse for `Yii:createObject()`. 14 | * Provide analyse for the last array argument of the `yii\base\Configurable` class constructor. 15 | * Mark `YII_*` constants as dynamic. 16 | * Significantly improves support for `ActiveRecord` and `ActiveQuery`. 17 | * Provides correct return type for `Yii::$container->get('service_id')` method. 18 | * Provides correct return type for `Yii::$app->request->headers->get('authorization')` method based on the `$first` parameter. 19 | * Provides reflection extension for `BaseObject`'s getters and setters. 20 | 21 | ## Installation 22 | 23 | To use this extension, require it in [Composer](https://getcomposer.org): 24 | 25 | ```sh 26 | composer require --dev erickskrauch/phpstan-yii2 27 | ``` 28 | 29 | If you also install [phpstan/extension-installer](https://github.com/phpstan/extension-installer) then you're all set! 30 | 31 |
32 | Manual installation 33 | 34 | If you don't want to use `phpstan/extension-installer`, include `extension.neon` in your project's PHPStan config: 35 | 36 | ``` 37 | includes: 38 | - vendor/erickskrauch/phpstan-yii2/extension.neon 39 | - vendor/erickskrauch/phpstan-yii2/rules.neon 40 | ``` 41 |
42 | 43 | ## Configuration 44 | 45 | You have to provide the path to the configuration file for your application. For [Advanced](https://github.com/yiisoft/yii2-app-advanced) project template your path might look like this: 46 | 47 | ```neon 48 | parameters: 49 | yii2: 50 | config_path: common/config/main.php 51 | ``` 52 | 53 | *You may want to create a separate configuration file for PHPStan describing the services available throughout the application. But usually, `common` is sufficient, because it contains all the services universally available in any module of the application.* 54 | 55 | [ico-version]: https://img.shields.io/packagist/v/erickskrauch/phpstan-yii2.svg?style=flat-square 56 | [ico-license]: https://img.shields.io/badge/license-MIT-green.svg?style=flat-square 57 | [ico-downloads]: https://img.shields.io/packagist/dt/erickskrauch/phpstan-yii2.svg?style=flat-square 58 | [ico-build-status]: https://img.shields.io/github/actions/workflow/status/erickskrauch/phpstan-yii2/ci.yml?branch=master&style=flat-square 59 | 60 | [link-packagist]: https://packagist.org/packages/erickskrauch/phpstan-yii2 61 | [link-downloads]: https://packagist.org/packages/erickskrauch/phpstan-yii2/stats 62 | [link-build-status]: https://github.com/erickskrauch/phpstan-yii2/actions 63 | -------------------------------------------------------------------------------- /src/Reflection/BaseObjectPropertiesClassReflectionExtension.php: -------------------------------------------------------------------------------- 1 | scope = new OutOfClassScope(); 18 | } 19 | 20 | public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { 21 | // The extension shouldn't work when phpdoc for the property is presented 22 | if (self::hasPropertyFromPhpDoc($classReflection, $propertyName)) { 23 | return false; 24 | } 25 | 26 | if ($classReflection->getName() !== BaseObject::class && !$classReflection->isSubclassOf(BaseObject::class)) { 27 | return false; 28 | } 29 | 30 | foreach (self::getNames($propertyName) as $methodName) { 31 | if (!$classReflection->hasMethod($methodName)) { 32 | continue; 33 | } 34 | 35 | $method = $classReflection->getMethod($methodName, $this->scope); 36 | if (!$method->isPublic() || $method->isStatic()) { 37 | continue; 38 | } 39 | 40 | return true; 41 | } 42 | 43 | return false; 44 | } 45 | 46 | public function getProperty(ClassReflection $classReflection, string $propertyName): BaseObjectPropertyReflection { 47 | [$getterName, $setterName] = self::getNames($propertyName); 48 | $getter = null; 49 | if ($classReflection->hasMethod($getterName)) { 50 | $method = $classReflection->getMethod($getterName, $this->scope); 51 | if ($method->isPublic() && !$method->isStatic()) { 52 | $getter = $method; 53 | } 54 | } 55 | 56 | $setter = null; 57 | if ($classReflection->hasMethod($setterName)) { 58 | $method = $classReflection->getMethod($setterName, $this->scope); 59 | if ($method->isPublic() && !$method->isStatic()) { 60 | $setter = $method; 61 | } 62 | } 63 | 64 | return new BaseObjectPropertyReflection($getter, $setter); 65 | } 66 | 67 | private static function hasPropertyFromPhpDoc(ClassReflection $classReflection, string $propertyName): bool { 68 | $phpDoc = $classReflection->getResolvedPhpDoc(); 69 | if ($phpDoc === null) { 70 | return false; 71 | } 72 | 73 | return isset($phpDoc->getPropertyTags()[$propertyName]); 74 | } 75 | 76 | /** 77 | * @return array{string, string} 78 | */ 79 | private static function getNames(string $propertyName): array { 80 | $cased = ucfirst($propertyName); // TODO: replace with mb analogue 81 | return ["get{$cased}", "set{$cased}"]; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/Rule/CreateConfigurableObjectRule.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class CreateConfigurableObjectRule implements Rule { 19 | 20 | private ReflectionProvider $reflectionProvider; 21 | 22 | private YiiConfigHelper $configHelper; 23 | 24 | public function __construct(ReflectionProvider $reflectionProvider, YiiConfigHelper $configHelper) { 25 | $this->reflectionProvider = $reflectionProvider; 26 | $this->configHelper = $configHelper; 27 | } 28 | 29 | public function getNodeType(): string { 30 | return New_::class; 31 | } 32 | 33 | /** 34 | * @param New_ $node 35 | */ 36 | public function processNode(Node $node, Scope $scope): array { 37 | $calledOn = $node->class; 38 | if (!$calledOn instanceof Node\Name) { 39 | return []; 40 | } 41 | 42 | $className = $calledOn->toString(); 43 | 44 | // Invalid call, leave it for another rules 45 | if (!$this->reflectionProvider->hasClass($className)) { 46 | return []; 47 | } 48 | 49 | $class = $this->reflectionProvider->getClass($className); 50 | // This rule intended for use only with Configurable interface 51 | if (!$class->is(Configurable::class)) { 52 | return []; 53 | } 54 | 55 | $constructorParams = ParametersAcceptorSelector::combineAcceptors($class->getConstructor()->getVariants())->getParameters(); 56 | $lastArgName = $constructorParams[array_key_last($constructorParams)]->getName(); 57 | 58 | $args = $node->args; 59 | foreach ($args as $arg) { 60 | // Try to find config by named argument 61 | if ($arg instanceof Node\Arg && $arg->name !== null && $arg->name->name === $lastArgName) { 62 | $configArg = $arg; 63 | break; 64 | } 65 | } 66 | 67 | // Attempt to find by named arg failed, try to find it by index 68 | if (!isset($configArg) && isset($args[count($constructorParams) - 1])) { 69 | $configArg = $args[count($constructorParams) - 1]; 70 | // At this moment I don't know what to do with variadic arguments 71 | if (!$configArg instanceof Node\Arg) { 72 | return []; 73 | } 74 | } 75 | 76 | // Config arg wasn't specified, so nothing to validate 77 | if (!isset($configArg)) { 78 | return []; 79 | } 80 | 81 | $objectType = new ObjectType($className); 82 | $configArgType = $scope->getType($configArg->value); 83 | $errors = []; 84 | foreach ($configArgType->getConstantArrays() as $constantArray) { 85 | $errors = array_merge($errors, $this->configHelper->validateArray($objectType, $constantArray, $scope)); 86 | } 87 | 88 | return $errors; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/Rule/CreateObjectRule.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class CreateObjectRule implements Rule { 18 | 19 | private ReflectionProvider $reflectionProvider; 20 | 21 | private YiiConfigHelper $configHelper; 22 | 23 | public function __construct(ReflectionProvider $reflectionProvider, YiiConfigHelper $configHelper) { 24 | $this->reflectionProvider = $reflectionProvider; 25 | $this->configHelper = $configHelper; 26 | } 27 | 28 | public function getNodeType(): string { 29 | return StaticCall::class; 30 | } 31 | 32 | /** 33 | * @param StaticCall $node 34 | */ 35 | public function processNode(Node $node, Scope $scope): array { 36 | $calledOn = $node->class; 37 | if (!$calledOn instanceof Node\Name) { 38 | return []; 39 | } 40 | 41 | $methodName = $node->name; 42 | if (!$methodName instanceof Node\Identifier) { 43 | return []; 44 | } 45 | 46 | if ($methodName->toString() !== 'createObject') { 47 | return []; 48 | } 49 | 50 | if (!$this->reflectionProvider->getClass($calledOn->toString())->is(BaseYii::class)) { 51 | return []; 52 | } 53 | 54 | $errors = []; 55 | $args = $node->getArgs(); 56 | // Probably invalid code so leave it to another rules 57 | if (count($args) < 1) { 58 | return []; 59 | } 60 | 61 | $firstArgType = $scope->getType($args[0]->value); 62 | if ($firstArgType->isConstantArray()->yes()) { 63 | /** @var \PHPStan\Type\Constant\ConstantArrayType $config */ 64 | $config = $firstArgType->getConstantArrays()[0]; 65 | $objectTypeOrError = $this->configHelper->findObjectType($config); 66 | if ($objectTypeOrError instanceof IdentifierRuleError) { 67 | return [$objectTypeOrError]; 68 | } 69 | 70 | $objectType = $objectTypeOrError; 71 | $errors = $this->configHelper->validateArray($objectTypeOrError, $config, $scope); 72 | } elseif ($firstArgType->isClassString()->yes()) { 73 | $objectType = $firstArgType->getClassStringObjectType(); 74 | } else { 75 | // We can't process second argument without knowing the class 76 | return []; 77 | } 78 | 79 | if (isset($args[1])) { 80 | // TODO: it is possible to pass both 2 argument and __construct() config param. 81 | // at the moment I'll not cover that case. 82 | // Note for future me 2nd argument value has priority when merging with __construct() 83 | $secondArgConstantArrays = $scope->getType($args[1]->value)->getConstantArrays(); 84 | if (count($secondArgConstantArrays) === 1) { 85 | $argsConfig = $secondArgConstantArrays[0]; 86 | $errors = array_merge($errors, $this->configHelper->validateConstructorArgs($objectType, $argsConfig, $scope)); 87 | } 88 | } 89 | 90 | return $errors; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/Type/ActiveRecordFindReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | reflectionProvider = $reflectionProvider; 31 | } 32 | 33 | public function getClass(): string { 34 | return ActiveRecordInterface::class; 35 | } 36 | 37 | public function isStaticMethodSupported(MethodReflection $methodReflection): bool { 38 | return in_array($methodReflection->getName(), ['find', 'findBySql', 'findByCondition'], true); 39 | } 40 | 41 | public function getTypeFromStaticMethodCall( 42 | MethodReflection $methodReflection, 43 | StaticCall $methodCall, 44 | Scope $scope 45 | ): ?Type { 46 | $calledOn = $methodCall->class; 47 | $declaringClass = $methodReflection->getDeclaringClass(); 48 | // The implementations of the ::findBySql() and ::findByCondition() methods rely on the ::find() method, 49 | // so unless they have been overridden, we return the ::find() method type 50 | if ($methodReflection->getName() !== 'find' && $declaringClass->getName() === ActiveRecord::class) { 51 | $findMethod = $declaringClass->getMethod('find', $scope); 52 | $findCall = new StaticCall($calledOn, 'find'); // According to the Yii2 implementation, this call will have no arguments 53 | 54 | return $this->getTypeFromStaticMethodCall($findMethod, $findCall, $scope); 55 | } 56 | 57 | if ($calledOn instanceof Name) { 58 | return $this->createType($scope->resolveName($calledOn), $methodReflection->getName(), $scope); 59 | } 60 | 61 | $types = []; 62 | if ($calledOn instanceof Variable) { 63 | foreach ($scope->getType($calledOn)->getConstantStrings() as $constantString) { 64 | if (!$constantString->isClassString()->yes()) { 65 | return new NeverType(); 66 | } 67 | 68 | $types[] = $this->createType($constantString->getValue(), $methodReflection->getName(), $scope); 69 | } 70 | 71 | return TypeCombinator::union(...$types); 72 | } 73 | 74 | return null; 75 | } 76 | 77 | private function createType(string $modelClass, string $methodName, Scope $scope): Type { 78 | $method = $this->reflectionProvider->getClass($modelClass)->getMethod($methodName, $scope); 79 | $returnType = ParametersAcceptorSelector::combineAcceptors($method->getVariants())->getReturnType(); 80 | if (!$returnType->isObject()->yes()) { 81 | throw new ShouldNotHappenException(); 82 | } 83 | 84 | $types = []; 85 | foreach ($returnType->getObjectClassNames() as $className) { 86 | $types[] = new ActiveQueryObjectType(new ActiveRecordObjectType($modelClass), $className); 87 | } 88 | 89 | return TypeCombinator::union(...$types); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /extension.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | yii2: 3 | config_path: null 4 | stubFiles: 5 | - src/Stubs/base/Action.stub 6 | - src/Stubs/base/ActionEvent.stub 7 | - src/Stubs/base/BaseObject.stub 8 | - src/Stubs/base/Behavior.stub 9 | - src/Stubs/base/Component.stub 10 | - src/Stubs/base/Controller.stub 11 | - src/Stubs/base/DynamicModel.stub 12 | - src/Stubs/base/Event.stub 13 | - src/Stubs/base/InlineAction.stub 14 | - src/Stubs/base/Model.stub 15 | - src/Stubs/base/Module.stub 16 | - src/Stubs/base/Request.stub 17 | - src/Stubs/base/Response.stub 18 | 19 | - src/Stubs/data/ActiveDataProvider.stub 20 | - src/Stubs/data/ArrayDataProvider.stub 21 | - src/Stubs/data/BaseDataProvider.stub 22 | - src/Stubs/data/DataProviderInterface.stub 23 | - src/Stubs/data/SqlDataProvider.stub 24 | 25 | - src/Stubs/db/ActiveQuery.stub 26 | - src/Stubs/db/ActiveRecord.stub 27 | - src/Stubs/db/BatchQueryResult.stub 28 | - src/Stubs/db/ColumnSchemaBuilder.stub 29 | - src/Stubs/db/Command.stub 30 | - src/Stubs/db/Connection.stub 31 | - src/Stubs/db/DataReader.stub 32 | - src/Stubs/db/Expression.stub 33 | - src/Stubs/db/Migration.stub 34 | - src/Stubs/db/QueryBuilder.stub 35 | - src/Stubs/db/SqlTokenizer.stub 36 | 37 | - src/Stubs/test/BaseActiveFixture.stub 38 | - src/Stubs/test/Fixture.stub 39 | 40 | - src/Stubs/validators/InlineValidator.stub 41 | - src/Stubs/validators/Validator.stub 42 | 43 | - src/Stubs/web/Cookie.stub 44 | - src/Stubs/web/CookieCollection.stub 45 | - src/Stubs/web/HeaderCollection.stub 46 | - src/Stubs/web/JsExpression.stub 47 | 48 | - src/Stubs/BaseYii.stub 49 | dynamicConstantNames: 50 | - YII_ENV 51 | - YII_ENV_PROD 52 | - YII_ENV_DEV 53 | - YII_ENV_TEST 54 | - YII_DEBUG 55 | 56 | parametersSchema: 57 | yii2: structure([ 58 | config_path: schema(string(), nullable()) 59 | ]) 60 | 61 | services: 62 | - class: ErickSkrauch\PHPStan\Yii2\Rule\YiiConfigHelper 63 | arguments: 64 | reportMaybes: %reportMaybes% 65 | 66 | - class: ErickSkrauch\PHPStan\Yii2\Reflection\ApplicationPropertiesClassReflectionExtension 67 | tags: [phpstan.broker.propertiesClassReflectionExtension] 68 | 69 | - class: ErickSkrauch\PHPStan\Yii2\Reflection\BaseObjectPropertiesClassReflectionExtension 70 | tags: [phpstan.broker.propertiesClassReflectionExtension] 71 | 72 | - class: ErickSkrauch\PHPStan\Yii2\Reflection\RequestMethodsClassReflectionExtension 73 | tags: [phpstan.broker.methodsClassReflectionExtension] 74 | 75 | - class: ErickSkrauch\PHPStan\Yii2\Reflection\RequestPropertiesClassReflectionExtension 76 | tags: [phpstan.broker.propertiesClassReflectionExtension] 77 | 78 | - class: ErickSkrauch\PHPStan\Yii2\Reflection\ResponsePropertiesClassReflectionExtension 79 | tags: [phpstan.broker.propertiesClassReflectionExtension] 80 | 81 | - class: ErickSkrauch\PHPStan\Yii2\Reflection\UserPropertiesClassReflectionExtension 82 | tags: [phpstan.broker.propertiesClassReflectionExtension] 83 | 84 | - class: ErickSkrauch\PHPStan\Yii2\Type\ActiveQueryBuilderReturnTypeExtension 85 | tags: [phpstan.broker.dynamicMethodReturnTypeExtension] 86 | 87 | - class: ErickSkrauch\PHPStan\Yii2\Type\ActiveRecordRelationReturnTypeExtension 88 | tags: [phpstan.broker.dynamicMethodReturnTypeExtension] 89 | 90 | - class: ErickSkrauch\PHPStan\Yii2\Type\ContainerDynamicMethodReturnTypeExtension 91 | tags: [phpstan.broker.dynamicMethodReturnTypeExtension] 92 | 93 | - class: ErickSkrauch\PHPStan\Yii2\Type\ActiveRecordFindReturnTypeExtension 94 | tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension] 95 | 96 | - class: ErickSkrauch\PHPStan\Yii2\Type\ActiveRecordRelationGetterReturnTypeExtension 97 | tags: [phpstan.broker.dynamicMethodReturnTypeExtension] 98 | 99 | - ErickSkrauch\PHPStan\Yii2\ServiceMap(%yii2.config_path%) 100 | -------------------------------------------------------------------------------- /src/Reflection/BaseObjectPropertyReflection.php: -------------------------------------------------------------------------------- 1 | getter = $getter; 23 | $this->setter = $setter; 24 | } 25 | 26 | public function getDeclaringClass(): ClassReflection { 27 | if ($this->getter) { 28 | return $this->getter->getDeclaringClass(); 29 | } 30 | 31 | if ($this->setter) { 32 | return $this->setter->getDeclaringClass(); 33 | } 34 | 35 | throw new ShouldNotHappenException('At least one getter or setter must be presented'); 36 | } 37 | 38 | public function isStatic(): bool { 39 | return false; 40 | } 41 | 42 | public function isPrivate(): bool { 43 | return false; 44 | } 45 | 46 | public function isPublic(): bool { 47 | return true; 48 | } 49 | 50 | public function getDocComment(): ?string { 51 | return null; 52 | } 53 | 54 | public function getReadableType(): Type { 55 | if ($this->getter === null) { 56 | return new NeverType(); 57 | } 58 | 59 | return ParametersAcceptorSelector::combineAcceptors($this->getter->getVariants())->getReturnType(); 60 | } 61 | 62 | public function getWritableType(): Type { 63 | if ($this->setter === null) { 64 | return new NeverType(); 65 | } 66 | 67 | /** @var \PHPStan\Reflection\ParameterReflection[] $params */ 68 | $params = ParametersAcceptorSelector::combineAcceptors($this->setter->getVariants())->getParameters(); 69 | if (!isset($params[0])) { 70 | throw new ShouldNotHappenException("Getter doesn't accept any arguments"); 71 | } 72 | 73 | return $params[0]->getType(); 74 | } 75 | 76 | public function canChangeTypeAfterAssignment(): bool { 77 | return false; 78 | } 79 | 80 | public function isReadable(): bool { 81 | return $this->getter !== null; 82 | } 83 | 84 | public function isWritable(): bool { 85 | return $this->setter !== null; 86 | } 87 | 88 | public function isDeprecated(): TrinaryLogic { 89 | $result = TrinaryLogic::createNo(); 90 | if ($this->getter !== null) { 91 | $result = $result->or($this->getter->isDeprecated()); 92 | } 93 | 94 | if ($this->setter !== null) { 95 | $result = $result->or($this->setter->isDeprecated()); 96 | } 97 | 98 | return $result; 99 | } 100 | 101 | public function getDeprecatedDescription(): ?string { 102 | if ($this->getter !== null && $this->getter->getDeprecatedDescription()) { 103 | return $this->getter->getDeprecatedDescription(); 104 | } 105 | 106 | if ($this->setter !== null && $this->setter->getDeprecatedDescription()) { 107 | return $this->setter->getDeprecatedDescription(); 108 | } 109 | 110 | return null; 111 | } 112 | 113 | public function isInternal(): TrinaryLogic { 114 | $result = TrinaryLogic::createNo(); 115 | if ($this->getter !== null) { 116 | $result = $result->or($this->getter->isInternal()); 117 | } 118 | 119 | if ($this->setter !== null) { 120 | $result = $result->or($this->setter->isInternal()); 121 | } 122 | 123 | return $result; 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/Type/ActiveQueryBuilderReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getVariants())->getReturnType(); 34 | if ((new ObjectType(ActiveQueryInterface::class))->isSuperTypeOf($type)->yes()) { 35 | return true; 36 | } 37 | 38 | return in_array($methodReflection->getName(), ['one', 'all', 'batch', 'each'], true); 39 | } 40 | 41 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { 42 | $calledOnType = $scope->getType($methodCall->var); 43 | if (!$calledOnType instanceof ActiveQueryObjectType) { 44 | return ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getReturnType(); 45 | } 46 | 47 | $methodName = $methodReflection->getName(); 48 | if ($methodName === 'asArray') { 49 | $argType = isset($methodCall->args[0]) && $methodCall->args[0] instanceof Arg 50 | ? $scope->getType($methodCall->args[0]->value) 51 | : new ConstantBooleanType(true); 52 | 53 | return new ActiveQueryObjectType( 54 | $calledOnType->getReturnType(), 55 | $calledOnType->getClassName(), 56 | $argType->isTrue()->yes(), 57 | $calledOnType->hasIndexBy(), 58 | ); 59 | } 60 | 61 | if ($methodName === 'indexBy') { 62 | $argType = $scope->getType($methodCall->getArgs()[0]->value); 63 | 64 | return new ActiveQueryObjectType( 65 | $calledOnType->getReturnType(), 66 | $calledOnType->getClassName(), 67 | $calledOnType->isAsArray(), 68 | !$argType->isNull()->yes(), 69 | ); 70 | } 71 | 72 | if ($methodName === 'one') { 73 | return TypeCombinator::union( 74 | new NullType(), 75 | $calledOnType->isAsArray() 76 | ? new ArrayType(new StringType(), new MixedType()) 77 | : $calledOnType->getReturnType(), 78 | ); 79 | } 80 | 81 | if ($methodName === 'all') { 82 | return new ArrayType( 83 | $calledOnType->hasIndexBy() ? new StringType() : new IntegerType(), 84 | $calledOnType->isAsArray() 85 | ? new ArrayType(new StringType(), new MixedType()) 86 | : $calledOnType->getReturnType(), 87 | ); 88 | } 89 | 90 | if ($methodName === 'batch') { 91 | return new GenericObjectType( 92 | BatchQueryResult::class, 93 | [ 94 | new IntegerType(), 95 | new ArrayType( 96 | $calledOnType->hasIndexBy() ? new StringType() : new IntegerType(), 97 | $calledOnType->isAsArray() 98 | ? new ArrayType(new StringType(), new MixedType()) 99 | : $calledOnType->getReturnType(), 100 | ), 101 | ], 102 | ); 103 | } 104 | 105 | if ($methodName === 'each') { 106 | return new GenericObjectType( 107 | BatchQueryResult::class, 108 | [ 109 | $calledOnType->hasIndexBy() ? new StringType() : new IntegerType(), 110 | $calledOnType->isAsArray() 111 | ? new ArrayType(new StringType(), new MixedType()) 112 | : $calledOnType->getReturnType(), 113 | ], 114 | ); 115 | } 116 | 117 | return $calledOnType; 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/ServiceMap.php: -------------------------------------------------------------------------------- 1 | , 21 | * definitions?: array, 22 | * }, 23 | * components?: array, 24 | * } 25 | */ 26 | final class ServiceMap { 27 | 28 | private const CORE_COMPONENTS = [ 29 | 'log' => \yii\log\Dispatcher::class, 30 | 'view' => \yii\web\View::class, 31 | 'formatter' => \yii\i18n\Formatter::class, 32 | 'i18n' => \yii\i18n\I18N::class, 33 | 'urlManager' => \yii\web\UrlManager::class, 34 | 'assetManager' => \yii\web\AssetManager::class, 35 | 'security' => \yii\base\Security::class, 36 | // TODO: Maybe in the future there will be a configuration which environment we want to analyze: web or console 37 | 'request' => \yii\web\Request::class, 38 | 'response' => \yii\web\Response::class, 39 | 'session' => \yii\web\Session::class, 40 | 'user' => \yii\web\User::class, 41 | 'errorHandler' => \yii\web\ErrorHandler::class, 42 | ]; 43 | 44 | /** 45 | * @var array 46 | */ 47 | private array $services = []; 48 | 49 | /** 50 | * @var array 51 | */ 52 | private array $components = []; 53 | 54 | /** 55 | * @throws \RuntimeException 56 | * @throws \ReflectionException 57 | */ 58 | public function __construct(string $configPath) { 59 | if (!file_exists($configPath)) { 60 | throw new InvalidArgumentException(sprintf('Provided config path %s must exist', $configPath)); 61 | } 62 | 63 | defined('YII_DEBUG') || define('YII_DEBUG', true); 64 | defined('YII_ENV_DEV') || define('YII_ENV_DEV', false); 65 | defined('YII_ENV_PROD') || define('YII_ENV_PROD', false); 66 | defined('YII_ENV_TEST') || define('YII_ENV_TEST', true); 67 | 68 | /** @var YII2ContainerConfig $config */ 69 | $config = require $configPath; 70 | foreach ($config['container']['singletons'] ?? [] as $id => $definition) { 71 | $this->services[$id] = $this->guessDefinition($id, $definition, $config); 72 | } 73 | 74 | foreach ($config['container']['definitions'] ?? [] as $id => $definition) { 75 | $this->services[$id] = $this->guessDefinition($id, $definition, $config); 76 | } 77 | 78 | foreach ($config['components'] ?? [] as $id => $definition) { 79 | $this->components[$id] = $this->guessDefinition($id, $definition, $config); 80 | } 81 | } 82 | 83 | public function getServiceClassFromNode(Node $node): ?string { 84 | if ($node instanceof String_) { 85 | $service = $node->value; 86 | } elseif ($node instanceof ClassConstFetch && $node->class instanceof Name) { 87 | $service = $node->class->toString(); 88 | } else { 89 | return null; 90 | } 91 | 92 | return $this->services[$service] ?? null; 93 | } 94 | 95 | public function getComponentClassById(string $id): ?string { 96 | return $this->components[$id] ?? self::CORE_COMPONENTS[$id] ?? null; 97 | } 98 | 99 | /** 100 | * @param Yii2Definition $definition 101 | * @param YII2ContainerConfig $config 102 | * 103 | * @throws \RuntimeException 104 | * @throws \ReflectionException 105 | */ 106 | private function guessDefinition(string $id, $definition, $config): string { 107 | if ($definition instanceof Closure) { 108 | $returnType = (new ReflectionFunction($definition))->getReturnType(); 109 | if ($returnType instanceof ReflectionNamedType) { 110 | return $returnType->getName(); 111 | } 112 | 113 | if (class_exists($id) || interface_exists($id)) { 114 | return $id; 115 | } 116 | 117 | throw new RuntimeException(sprintf('Please provide return type for %s service closure', $id)); 118 | } 119 | 120 | if (is_object($definition)) { 121 | return get_class($definition); 122 | } 123 | 124 | if (is_array($definition)) { 125 | if (isset($definition['class'])) { 126 | return $definition['class']; 127 | } 128 | 129 | if (isset($definition['__class'])) { 130 | return $definition['__class']; 131 | } 132 | 133 | if (isset(self::CORE_COMPONENTS[$id])) { 134 | return self::CORE_COMPONENTS[$id]; 135 | } 136 | } 137 | 138 | if (is_string($definition)) { 139 | if (isset($config['container']['definitions'][$definition])) { 140 | return $this->guessDefinition($definition, $config['container']['definitions'][$definition], $config); 141 | } 142 | 143 | if (isset($config['container']['singletons'][$definition])) { 144 | return $this->guessDefinition($definition, $config['container']['singletons'][$definition], $config); 145 | } 146 | 147 | if (class_exists($definition) || interface_exists($definition)) { 148 | return $definition; 149 | } 150 | } 151 | 152 | if (class_exists($id)) { 153 | return $id; 154 | } 155 | 156 | throw new RuntimeException(sprintf('Unsupported definition for %s', $id)); 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /src/Rule/YiiConfigHelper.php: -------------------------------------------------------------------------------- 1 | ruleLevelHelper = $ruleLevelHelper; 30 | $this->reportMaybes = $reportMaybes; 31 | } 32 | 33 | /** 34 | * @return Type|\PHPStan\Rules\IdentifierRuleError 35 | */ 36 | public function findObjectType(ConstantArrayType $config) { 37 | foreach (['__class', 'class'] as $classKey) { 38 | $classType = $config->getOffsetValueType(new ConstantStringType($classKey)); 39 | // This condition will also filter our invalid type, which should be already reported by PHPStan itself 40 | if (!$classType->isClassString()->yes()) { 41 | continue; 42 | } 43 | 44 | return $classType->getClassStringObjectType(); 45 | } 46 | 47 | return RuleErrorBuilder::message('Configuration params array must have "class" or "__class" key') 48 | ->identifier('yiiConfig.missingClass') 49 | ->build(); 50 | } 51 | 52 | /** 53 | * @phpstan-return list<\PHPStan\Rules\IdentifierRuleError> 54 | */ 55 | public function validateArray(Type $object, ConstantArrayType $config, Scope $scope): array { 56 | $errors = []; 57 | /** @var ConstantIntegerType|ConstantStringType $key */ 58 | foreach ($config->getKeyTypes() as $i => $key) { 59 | /** @var Type $value */ 60 | $value = $config->getValueTypes()[$i]; 61 | // @phpstan-ignore-next-line according to getKeyType() typing it is only possible to have those or ConstantIntType 62 | if (!$key instanceof ConstantStringType) { 63 | $errors[] = RuleErrorBuilder::message('The object configuration params must be indexed by name') 64 | ->identifier('argument.type') 65 | ->build(); 66 | continue; 67 | } 68 | 69 | $propertyName = $key->getValue(); 70 | // Skip class name declaration since it's already readed 71 | if ($propertyName === 'class' || $propertyName === '__class') { 72 | continue; 73 | } 74 | 75 | // TODO: yii\base\Configurable interface 76 | if ($propertyName === '__construct()') { 77 | if (!$value->isConstantArray()->yes()) { 78 | $errors[] = RuleErrorBuilder::message(sprintf( 79 | 'The constructor params must be an array, %s given', 80 | $value->describe(VerbosityLevel::typeOnly()), 81 | )) 82 | ->identifier('argument.type') 83 | ->build(); 84 | continue; 85 | } 86 | 87 | $errors = array_merge($errors, $this->validateConstructorArgs($object, $value->getConstantArrays()[0], $scope)); 88 | continue; 89 | } 90 | 91 | // Skip behaviors and events attachment 92 | if (str_starts_with($propertyName, 'as ') || str_starts_with($propertyName, 'on ')) { 93 | // TODO: if it's not Configurable, than we're in trouble 94 | continue; 95 | } 96 | 97 | $typeResult = $this->ruleLevelHelper->findTypeToCheck( 98 | $scope, 99 | new TypeExpr($object), 100 | sprintf('Access to property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($propertyName)), // @phpstan-ignore-line @ondrejmirtes said that I can use that method 101 | static fn(Type $type): bool => $type->canAccessProperties()->yes() && $type->hasProperty($propertyName)->yes(), 102 | ); 103 | $objectType = $typeResult->getType(); 104 | if ($objectType instanceof ErrorType) { 105 | return $typeResult->getUnknownClassErrors(); 106 | } 107 | 108 | if (!$objectType->canAccessProperties()->yes() || !$objectType->hasProperty($propertyName)->yes()) { 109 | $errors[] = RuleErrorBuilder::message(sprintf( 110 | "The config for %s is wrong: the property %s doesn't exists", 111 | $objectType->describe(VerbosityLevel::typeOnly()), 112 | $propertyName, 113 | )) 114 | ->identifier('property.notFound') 115 | ->build(); 116 | continue; 117 | } 118 | 119 | $property = $objectType->getProperty($propertyName, $scope); 120 | if (!$property->isPublic()) { 121 | $errors[] = RuleErrorBuilder::message(sprintf( 122 | 'Access to %s property %s::$%s.', 123 | $property->isPrivate() ? 'private' : 'protected', 124 | $property->getDeclaringClass()->getName(), 125 | $propertyName, 126 | )) 127 | ->identifier('property.private') 128 | ->build(); 129 | continue; 130 | } 131 | 132 | if (!$property->isWritable()) { 133 | $errors[] = RuleErrorBuilder::message(sprintf( 134 | 'Property %s::$%s is not writable.', 135 | $property->getDeclaringClass()->getName(), 136 | $propertyName, 137 | )) 138 | ->identifier('assign.propertyReadOnly') 139 | ->build(); 140 | continue; 141 | } 142 | 143 | $target = $property->getWritableType(); 144 | $result = $this->ruleLevelHelper->accepts($target, $value, $scope->isDeclareStrictTypes()); 145 | if (!$result->result) { 146 | $level = VerbosityLevel::getRecommendedLevelByType($target, $value); 147 | $errors[] = RuleErrorBuilder::message(sprintf( 148 | 'Property %s::$%s (%s) does not accept %s.', 149 | $property->getDeclaringClass()->getDisplayName(), 150 | $propertyName, 151 | $target->describe($level), 152 | $value->describe($level), 153 | )) 154 | ->identifier('assign.propertyType') 155 | ->acceptsReasonsTip($result->reasons) 156 | ->build(); 157 | } 158 | } 159 | 160 | return $errors; 161 | } 162 | 163 | /** 164 | * @phpstan-return list<\PHPStan\Rules\IdentifierRuleError> 165 | */ 166 | public function validateConstructorArgs(Type $object, ConstantArrayType $config, Scope $scope): array { 167 | /** @var \PHPStan\Type\Type|null $firstParamKeyType */ 168 | $firstParamKeyType = null; 169 | $errors = []; 170 | 171 | /** @var ConstantIntegerType|ConstantStringType $key */ 172 | foreach ($config->getKeyTypes() as $i => $key) { 173 | /** @var \PHPStan\Type\Type $paramValue */ 174 | $paramValue = $config->getValueTypes()[$i]; 175 | $paramName = $key->getValue(); 176 | $paramStrToReport = is_int($paramName) ? ('#' . ($paramName + 1)) : ('$' . $paramName); 177 | 178 | if ($firstParamKeyType === null) { 179 | $firstParamKeyType = $key; 180 | } elseif (!$firstParamKeyType instanceof $key) { 181 | $errors[] = RuleErrorBuilder::message("Parameters indexed by name and by position in the same array aren't allowed.") 182 | ->identifier('yiiConfig.constructorParamsMix') 183 | ->build(); 184 | continue; 185 | } 186 | 187 | /** @var list<\PHPStan\Rules\IdentifierRuleError> $paramSearchErrors */ 188 | $paramSearchErrors = []; 189 | /** @var list $foundParams */ 190 | $foundParams = []; 191 | /** @var \PHPStan\Reflection\ClassReflection $classReflection */ 192 | foreach ($object->getObjectClassReflections() as $classReflection) { 193 | $constructorParams = ParametersAcceptorSelector::combineAcceptors($classReflection->getConstructor()->getVariants())->getParameters(); 194 | $param = null; 195 | $paramIndex = 0; 196 | 197 | // TODO: prevent direct pass of 'config' param to constructor args (\yii\base\Configurable) 198 | 199 | if (is_int($paramName)) { 200 | $param = $constructorParams[$paramName] ?? null; 201 | $paramIndex = $paramName; 202 | } else { 203 | foreach ($constructorParams as $j => $constructorParam) { 204 | if ($constructorParam->getName() === $paramName) { 205 | $param = $constructorParam; 206 | $paramIndex = $j; 207 | break; 208 | } 209 | } 210 | } 211 | 212 | if ($param === null) { 213 | $paramSearchErrors[] = RuleErrorBuilder::message(sprintf( 214 | 'Unknown parameter %s in call to %s constructor.', 215 | $paramStrToReport, 216 | $classReflection->getName(), 217 | )) 218 | ->identifier('argument.unknown') 219 | ->build(); 220 | continue; 221 | } 222 | 223 | $foundParams[] = [$classReflection->getName(), $param, sprintf('#%s $%s', $paramIndex + 1, $param->getName())]; 224 | } 225 | 226 | if (empty($foundParams)) { 227 | $errors[] = RuleErrorBuilder::message(sprintf( 228 | 'Unknown parameter %s in call to %s constructor.', 229 | $paramStrToReport, 230 | $object->describe(VerbosityLevel::typeOnly()), 231 | )) 232 | ->identifier('argument.unknown') 233 | ->build(); 234 | continue; 235 | } 236 | 237 | if ($this->reportMaybes && !empty($paramSearchErrors)) { 238 | $errors = array_merge($errors, $paramSearchErrors); 239 | continue; 240 | } 241 | 242 | /** @var list<\PHPStan\Rules\IdentifierRuleError> $typeCheckErrors */ 243 | $typeCheckErrors = []; 244 | $accepted = false; 245 | foreach ($foundParams as [$className, $constructorParam, $strToReport]) { 246 | $paramType = $constructorParam->getType(); 247 | $result = $this->ruleLevelHelper->accepts($paramType, $paramValue, $scope->isDeclareStrictTypes()); 248 | if (!$result->result) { 249 | $level = VerbosityLevel::getRecommendedLevelByType($paramType, $paramValue); 250 | $typeCheckErrors[] = RuleErrorBuilder::message(sprintf( 251 | 'Parameter %s of class %s constructor expects %s, %s given.', 252 | $strToReport, 253 | $className, 254 | $paramType->describe($level), 255 | $paramValue->describe($level), 256 | )) 257 | ->identifier('argument.type') 258 | ->acceptsReasonsTip($result->reasons) 259 | ->build(); 260 | } 261 | 262 | $accepted |= $result->result; 263 | } 264 | 265 | if (!$accepted) { 266 | // TODO: bad decision, need to create more specific error 267 | $errors[] = $typeCheckErrors[0]; 268 | continue; 269 | } 270 | 271 | if ($this->reportMaybes && !empty($typeCheckErrors)) { 272 | $errors = array_merge($errors, $typeCheckErrors); 273 | } 274 | } 275 | 276 | return $errors; 277 | } 278 | 279 | } 280 | --------------------------------------------------------------------------------