├── CHANGELOG.md ├── LICENSE ├── README.md ├── bootstrap.php ├── composer.json ├── docs └── type-inference.md ├── extension.neon └── src ├── NodeVisitor └── ModelReturnTypeTransformVisitor.php ├── Rules ├── Classes │ ├── CacheHandlerInstantiationRule.php │ └── FrameworkExceptionInstantiationRule.php ├── Functions │ ├── FactoriesFunctionArgumentTypeRule.php │ ├── NoClassConstFetchOnFactoriesFunctions.php │ └── ServicesFunctionArgumentTypeRule.php └── Superglobals │ ├── SuperglobalAccessRule.php │ ├── SuperglobalAssignRule.php │ └── SuperglobalRuleHelper.php └── Type ├── CacheFactoryGetHandlerReturnTypeExtension.php ├── FactoriesFunctionReturnTypeExtension.php ├── FactoriesReturnTypeHelper.php ├── FakeFunctionReturnTypeExtension.php ├── ModelFetchedReturnTypeHelper.php ├── ModelFindReturnTypeExtension.php ├── ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension.php ├── ServicesFunctionReturnTypeExtension.php ├── ServicesGetSharedInstanceReturnTypeExtension.php └── ServicesReturnTypeHelper.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All releases have their changelog [published in the Releases section](https://github.com/CodeIgniter/phpstan-codeigniter/releases). 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 CodeIgniter Foundation and John Paul E. Balandan, CPA 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeIgniter extensions and rules for PHPStan 2 | 3 | [![Extension Tests](https://github.com/CodeIgniter/phpstan-codeigniter/actions/workflows/test-phpunit.yml/badge.svg)](https://github.com/CodeIgniter/phpstan-codeigniter/actions/workflows/test-phpunit.yml) 4 | [![Coding Standards Check](https://github.com/CodeIgniter/phpstan-codeigniter/actions/workflows/test-coding-standards.yml/badge.svg)](https://github.com/CodeIgniter/phpstan-codeigniter/actions/workflows/test-coding-standards.yml) 5 | [![PHPStan Static Analysis](https://github.com/CodeIgniter/phpstan-codeigniter/actions/workflows/test-phpstan.yml/badge.svg)](https://github.com/CodeIgniter/phpstan-codeigniter/actions/workflows/test-phpstan.yml) 6 | 7 | * [PHPStan](https://phpstan.org/) 8 | * [CodeIgniter](https://codeigniter.com/) 9 | 10 | This extension provides the following features: 11 | 12 | * [Type inference](docs/type-inference.md) 13 | 14 | ### Rules 15 | 16 | * Checks if the string argument passed to `config()` or `model()` function is a valid class string extending 17 | `CodeIgniter\Config\BaseConfig` or `CodeIgniter\Model`, respectively. This can be turned off by setting 18 | `codeigniter.checkArgumentTypeOfFactories: false` in your `phpstan.neon`. For fine-grained control, you can 19 | individually choose which factory function to disable using `codeigniter.checkArgumentTypeOfConfig` and 20 | `codeigniter.checkArgumentTypeOfModel`. **NOTE:** Setting `codeigniter.checkArgumentTypeOfFactories: false` will effectively 21 | bypass the two specific options. 22 | * Checks if the string argument passed to `service()` or `single_service()` function is a valid service name. This can be turned off by setting `codeigniter.checkArgumentTypeOfServices: false` in your `phpstan.neon`. 23 | * Disallows instantiating cache handlers using `new` and suggests to use the `CacheFactory` class instead. 24 | * Disallows instantiating `FrameworkException` classes using `new`. 25 | * Disallows direct re-assignment or access of `$_SERVER` and `$_GET` and suggests to use the `Superglobals` class instead. 26 | * Disallows use of `::class` fetch on `config()` and `model()` and suggests to use the short form of the class instead. 27 | 28 | ## Installation 29 | 30 | To use this extension, require it in [Composer](https://getcomposer.org/): 31 | 32 | ``` 33 | composer require --dev codeigniter/phpstan-codeigniter 34 | ``` 35 | 36 | If you also install [phpstan/extension-installer](https://github.com/phpstan/extension-installer) then you're all set! 37 | 38 |
39 | Manual installation 40 | 41 | If you don't want to use `phpstan/extension-installer`, include extension.neon in your project's PHPStan config: 42 | 43 | ```yml 44 | includes: 45 | - vendor/codeigniter/phpstan-codeigniter/extension.neon 46 | ``` 47 | 48 |
49 | 50 | ## Contributing 51 | 52 | Any contributions are welcome. 53 | 54 | If you want to see a new rule or extension specific to CodeIgniter, please open a 55 | [feature request](https://github.com/CodeIgniter/phpstan-codeigniter/issues/new?assignees=&labels=feature+request&projects=&template=feature_request.yml). If you can contribute the code yourself, please open a pull request instead. 56 | 57 | Before reporting any bugs, please check if the bug occurs only if using this extension with PHPStan. If the bug is 58 | reproducible in PHPStan alone, please open a bug report there instead. Thank you! 59 | 60 | ## License 61 | 62 | PHPStan CodeIgniter is an open source library licensed under [MIT](LICENSE). 63 | -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | require_once __DIR__ . '/vendor/codeigniter4/framework/system/Test/bootstrap.php'; 15 | 16 | foreach ([ 17 | 'vendor/codeigniter4/framework/app/Config', 18 | 'vendor/codeigniter4/framework/system/Helpers' 19 | ] as $directory) { 20 | $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)); 21 | 22 | /** @var SplFileInfo $file */ 23 | foreach ($iterator as $file) { 24 | if ($file->isFile() && $file->getExtension() === 'php') { 25 | require_once $file->getRealPath(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeigniter/phpstan-codeigniter", 3 | "description": "CodeIgniter extensions and rules for PHPStan", 4 | "license": "MIT", 5 | "type": "phpstan-extension", 6 | "keywords": [ 7 | "codeigniter", 8 | "codeigniter4", 9 | "dev", 10 | "phpstan", 11 | "static analysis" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "John Paul E. Balandan, CPA", 16 | "email": "paulbalandan@gmail.com" 17 | } 18 | ], 19 | "support": { 20 | "forum": "http://forum.codeigniter.com/", 21 | "source": "https://github.com/CodeIgniter/phpstan-codeigniter", 22 | "slack": "https://codeigniterchat.slack.com" 23 | }, 24 | "require": { 25 | "php": "^8.1", 26 | "phpstan/phpstan": "^2.0" 27 | }, 28 | "require-dev": { 29 | "codeigniter4/framework": "^4.5", 30 | "codeigniter/coding-standard": "^1.7", 31 | "codeigniter4/shield": "^1.0", 32 | "friendsofphp/php-cs-fixer": "^3.49", 33 | "nexusphp/cs-config": "^3.21", 34 | "phpstan/extension-installer": "^1.3", 35 | "phpstan/phpstan-deprecation-rules": "^2.0", 36 | "phpstan/phpstan-phpunit": "^2.0", 37 | "phpstan/phpstan-strict-rules": "^2.0", 38 | "phpunit/phpunit": "^10.5 || ^11.4" 39 | }, 40 | "conflict": { 41 | "codeigniter/framework": "*" 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true, 45 | "autoload": { 46 | "psr-4": { 47 | "CodeIgniter\\PHPStan\\": "src/" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "classmap": [ 52 | "tests/" 53 | ] 54 | }, 55 | "config": { 56 | "allow-plugins": { 57 | "phpstan/extension-installer": true 58 | }, 59 | "optimize-autoloader": true, 60 | "preferred-install": "dist", 61 | "sort-packages": true 62 | }, 63 | "extra": { 64 | "phpstan": { 65 | "includes": [ 66 | "extension.neon" 67 | ] 68 | } 69 | }, 70 | "scripts": { 71 | "post-update-cmd": [ 72 | "CodeIgniter\\PHPStan\\ComposerScripts::postUpdate" 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /docs/type-inference.md: -------------------------------------------------------------------------------- 1 | # Type Inference 2 | 3 | All type inference capabilities of this extension are summarised below: 4 | 5 | ## Dynamic Function Return Type Extensions 6 | 7 | ### FactoriesFunctionReturnTypeExtension 8 | 9 | This extension provides precise return types for the `config()` and `model()` functions. 10 | 11 | **Before:** 12 | ```php 13 | \PHPStan\dumpType(config('bar')); // BaseConfig|null 14 | \PHPStan\dumpType(config('App')); // BaseConfig|null 15 | \PHPStan\dumpType(model(BarModel::class)); // Model|null 16 | 17 | ``` 18 | 19 | **After:** 20 | ```php 21 | \PHPStan\dumpType(config('bar')); // null 22 | \PHPStan\dumpType(config('App')); // Config\App 23 | \PHPStan\dumpType(model(BarModel::class)); // CodeIgniter\PHPStan\Tests\Fixtures\Type\BarModel 24 | 25 | ``` 26 | 27 | > [!NOTE] 28 | > **Configuration:** 29 | > 30 | > This extension adds the default namespace for `config()` and `model()` functions as `Config\` 31 | > and `App\Models\`, respectively, when searching for possible classes. If your application uses 32 | > other namespaces, you can configure this extension in your `phpstan.neon` to recognize those namespaces: 33 | > 34 | > ```yml 35 | > parameters: 36 | > codeigniter: 37 | > additionalConfigNamespaces: 38 | > - Acme\Blog\Config\ 39 | > - Foo\Bar\Config\ 40 | > additionalModelNamespaces: 41 | > - Acme\Blog\Models\ 42 | > 43 | > ``` 44 | 45 | ### FakeFunctionReturnTypeExtension 46 | 47 | This extension provides precise return type for the `fake()` function. 48 | 49 | **Before:** 50 | ```php 51 | \PHPStan\dumpType(fake('baz')); // array|object 52 | \PHPStan\dumpType(fake(BarModel::class)); // array|object 53 | \PHPStan\dumpType(fake(UserModel::class)); // array|object 54 | \PHPStan\dumpType(fake(UserIdentityModel::class)); // array|object 55 | \PHPStan\dumpType(fake(LoginModel::class)); // array|object 56 | \PHPStan\dumpType(fake(TokenLoginModel::class)); // array|object 57 | \PHPStan\dumpType(fake(GroupModel::class)); // array|object 58 | 59 | ``` 60 | 61 | **After:** 62 | ```php 63 | \PHPStan\dumpType(fake('baz')); // never 64 | \PHPStan\dumpType(fake(BarModel::class)); // stdClass 65 | \PHPStan\dumpType(fake(UserModel::class)); // CodeIgniter\Shield\Entities\User 66 | \PHPStan\dumpType(fake(UserIdentityModel::class)); // CodeIgniter\Shield\Entities\UserIdentity 67 | \PHPStan\dumpType(fake(LoginModel::class)); // CodeIgniter\Shield\Entities\Login 68 | \PHPStan\dumpType(fake(TokenLoginModel::class)); // CodeIgniter\Shield\Entities\Login 69 | \PHPStan\dumpType(fake(GroupModel::class)); // array{user_id: int, group: string, created_at: string} 70 | 71 | ``` 72 | 73 | > [!NOTE] 74 | > **Configuration:** 75 | > 76 | > When the model passed to `fake()` has the property `$returnType` set to `array`, this extension will give 77 | > a precise array shape based on the allowed fields of the model. Most of the time, the formatted fields are 78 | > strings. If not a string, you can indicate the format return type for the particular field. 79 | > 80 | > ```yml 81 | > parameters: 82 | > codeigniter: 83 | > notStringFormattedFields: # key-value pair of field => format 84 | > success: bool 85 | > user_id: int 86 | > ``` 87 | 88 | ### ServicesFunctionReturnTypeExtension 89 | 90 | This extension provides precise return types for the `service()` and `single_service()` functions. 91 | 92 | **Before:** 93 | ```php 94 | \PHPStan\dumpType(service('cache')); // object|null 95 | 96 | ``` 97 | 98 | **After:** 99 | ```php 100 | \PHPStan\dumpType(service('cache')); // CodeIgniter\Cache\CacheInterface 101 | ``` 102 | 103 | > [!NOTE] 104 | > **Configuration:** 105 | > 106 | > You can instruct PHPStan to consider your own services factory classes. 107 | > **Please note that it should be a valid class extending `CodeIgniter\Config\BaseService`!** 108 | > 109 | > ```yml 110 | > parameters: 111 | > codeigniter: 112 | > additionalServices: 113 | > - Acme\Blog\Config\ServiceFactory 114 | > ``` 115 | 116 | ## Dynamic Method Return Type Extension 117 | 118 | ### ModelFindReturnTypeExtension 119 | 120 | This extension provides precise return types for `CodeIgniter\Model`'s `find()`, `findAll()`, and `first()` methods. 121 | This also allows dynamic return type transformation of `CodeIgniter\Model` when `asArray()` or `asObject()` is called. 122 | 123 | ## Dynamic Static Method Return Type Extensions 124 | 125 | ### CacheFactoryHandlerReturnTypeExtension 126 | 127 | This extension provides precise return type to `CacheFactory::getHandler()` static method. 128 | 129 | **Before:** 130 | ```php 131 | \PHPStan\dumpType(CacheFactory::getHandler(new Cache())); // CodeIgniter\Cache\CacheInterface 132 | \PHPStan\dumpType(CacheFactory::getHandler(new Cache(), 'redis')); // CodeIgniter\Cache\CacheInterface 133 | ``` 134 | 135 | **After:** 136 | ```php 137 | \PHPStan\dumpType(CacheFactory::getHandler(new Cache())); // CodeIgniter\Cache\Handlers\FileHandler 138 | \PHPStan\dumpType(CacheFactory::getHandler(new Cache(), 'redis')); // CodeIgniter\Cache\Handlers\RedisHandler 139 | ``` 140 | 141 | > [!NOTE] 142 | > **Configuration:** 143 | > 144 | > By default, this extension only considers the primary handler as the return type. If that fails (e.g. the handler 145 | > is not defined in the Cache config's `$validHandlers` array), then this will return the backup handler as 146 | > return type. If you want to return both primary and backup handlers as return type, you can set this: 147 | > 148 | > ```yml 149 | > parameters: 150 | > codeigniter: 151 | > addBackupHandlerAsReturnType: true 152 | > ``` 153 | > 154 | > This setting will give the return type as a benevolent union of the primary and backup handler types. 155 | > 156 | > ```php 157 | > \PHPStan\dumpType(CacheFactory::getHandler(new Cache())); // (FileHandler|DummyHandler) 158 | > \PHPStan\dumpType(CacheFactory::getHandler(new Cache(), 'redis', 'file')); // (FileHandler|RedisHandler) 159 | > ``` 160 | 161 | ### ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension 162 | 163 | This extension provides precise return type to `ReflectionHelper`'s static `getPrivateMethodInvoker()` method. 164 | Since PHPStan's dynamic return type extensions work on classes, not traits, this extension is on by default 165 | in test cases extending `CodeIgniter\Test\CIUnitTestCase`. To make this work, you should be calling the method 166 | **statically**: 167 | 168 | For example, we're accessing the private method: 169 | ```php 170 | class Foo 171 | { 172 | private static function privateMethod(string $value): bool 173 | { 174 | return true; 175 | } 176 | } 177 | 178 | ``` 179 | 180 | **Before:** 181 | ```php 182 | public function testSomePrivateMethod(): void 183 | { 184 | $method = self::getPrivateMethodInvoker(new Foo(), 'privateMethod'); 185 | \PHPStan\dumpType($method); // Closure(mixed ...): mixed 186 | } 187 | 188 | ``` 189 | 190 | **After:** 191 | ```php 192 | public function testSomePrivateMethod(): void 193 | { 194 | $method = self::getPrivateMethodInvoker(new Foo(), 'privateMethod'); 195 | \PHPStan\dumpType($method); // Closure(string): bool 196 | } 197 | 198 | ``` 199 | 200 | > [!NOTE] 201 | > **Configuration:** 202 | > 203 | > If you are using `ReflectionHelper` outside of testing, you can still enjoy the precise return types by adding a 204 | > service for the class using this trait. In your `phpstan.neon` (or `phpstan.neon.dist`), add the following to 205 | > the _**services**_ schema: 206 | > 207 | > ```yml 208 | > - 209 | > class: CodeIgniter\PHPStan\Type\ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension 210 | > tags: 211 | > - phpstan.broker.dynamicStaticMethodReturnTypeExtension 212 | > arguments: 213 | > class: 214 | > 215 | > ``` 216 | 217 | ### ServicesGetSharedInstanceReturnTypeExtension 218 | 219 | This extension provides precise return type for the protected static method `getSharedInstance()` of `Services`. 220 | 221 | **Before:** 222 | ```php 223 | [!NOTE] 258 | > **Configuration:** 259 | > 260 | > You can instruct PHPStan to consider your own services factory classes. 261 | > **Please note that it should be a valid class extending `CodeIgniter\Config\BaseService`!** 262 | > 263 | > ```yml 264 | > parameters: 265 | > codeigniter: 266 | > additionalServices: 267 | > - Acme\Blog\Config\ServiceFactory 268 | > ``` 269 | -------------------------------------------------------------------------------- /extension.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | dynamicConstantNames: 3 | - APP_NAMESPACE 4 | - CI_DEBUG 5 | - ENVIRONMENT 6 | universalObjectCratesClasses: 7 | - CodeIgniter\Entity\Entity 8 | codeigniter: 9 | additionalConfigNamespaces: 10 | - CodeIgniter\Config\ 11 | additionalModelNamespaces: [] 12 | additionalServices: [] 13 | addBackupHandlerAsReturnType: false 14 | notStringFormattedFields: [] 15 | checkArgumentTypeOfFactories: true 16 | checkArgumentTypeOfConfig: true 17 | checkArgumentTypeOfModel: true 18 | checkArgumentTypeOfServices: true 19 | 20 | parametersSchema: 21 | codeigniter: structure([ 22 | additionalConfigNamespaces: listOf(string()) 23 | additionalModelNamespaces: listOf(string()) 24 | additionalServices: listOf(string()) 25 | addBackupHandlerAsReturnType: bool() 26 | notStringFormattedFields: arrayOf(string()) 27 | checkArgumentTypeOfFactories: bool() 28 | checkArgumentTypeOfConfig: bool() 29 | checkArgumentTypeOfModel: bool() 30 | checkArgumentTypeOfServices: bool() 31 | ]) 32 | 33 | services: 34 | # helpers 35 | factoriesReturnTypeHelper: 36 | class: CodeIgniter\PHPStan\Type\FactoriesReturnTypeHelper 37 | arguments: 38 | additionalConfigNamespaces: %codeigniter.additionalConfigNamespaces% 39 | additionalModelNamespaces: %codeigniter.additionalModelNamespaces% 40 | 41 | modelFetchedReturnTypeHelper: 42 | class: CodeIgniter\PHPStan\Type\ModelFetchedReturnTypeHelper 43 | arguments: 44 | notStringFormattedFieldsArray: %codeigniter.notStringFormattedFields% 45 | 46 | servicesReturnTypeHelper: 47 | class: CodeIgniter\PHPStan\Type\ServicesReturnTypeHelper 48 | arguments: 49 | additionalServices: %codeigniter.additionalServices% 50 | 51 | superglobalRuleHelper: 52 | class: CodeIgniter\PHPStan\Rules\Superglobals\SuperglobalRuleHelper 53 | 54 | # Node Visitors 55 | - 56 | class: CodeIgniter\PHPStan\NodeVisitor\ModelReturnTypeTransformVisitor 57 | tags: 58 | - phpstan.parser.richParserNodeVisitor 59 | 60 | # DynamicFunctionReturnTypeExtension 61 | - 62 | class: CodeIgniter\PHPStan\Type\FactoriesFunctionReturnTypeExtension 63 | tags: 64 | - phpstan.broker.dynamicFunctionReturnTypeExtension 65 | 66 | - 67 | class: CodeIgniter\PHPStan\Type\FakeFunctionReturnTypeExtension 68 | tags: 69 | - phpstan.broker.dynamicFunctionReturnTypeExtension 70 | 71 | - 72 | class: CodeIgniter\PHPStan\Type\ServicesFunctionReturnTypeExtension 73 | tags: 74 | - phpstan.broker.dynamicFunctionReturnTypeExtension 75 | 76 | # DynamicMethodReturnTypeExtension 77 | - 78 | class: CodeIgniter\PHPStan\Type\ModelFindReturnTypeExtension 79 | tags: 80 | - phpstan.broker.dynamicMethodReturnTypeExtension 81 | 82 | # DynamicStaticMethodReturnTypeExtension 83 | - 84 | class: CodeIgniter\PHPStan\Type\CacheFactoryGetHandlerReturnTypeExtension 85 | tags: 86 | - phpstan.broker.dynamicStaticMethodReturnTypeExtension 87 | arguments: 88 | addBackupHandlerAsReturnType: %codeigniter.addBackupHandlerAsReturnType% 89 | 90 | - 91 | class: CodeIgniter\PHPStan\Type\ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension 92 | tags: 93 | - phpstan.broker.dynamicStaticMethodReturnTypeExtension 94 | arguments: 95 | class: CodeIgniter\Test\CIUnitTestCase 96 | 97 | - 98 | class: CodeIgniter\PHPStan\Type\ServicesGetSharedInstanceReturnTypeExtension 99 | tags: 100 | - phpstan.broker.dynamicStaticMethodReturnTypeExtension 101 | 102 | # conditional rules 103 | - 104 | class: CodeIgniter\PHPStan\Rules\Functions\FactoriesFunctionArgumentTypeRule 105 | arguments: 106 | checkArgumentTypeOfConfig: %codeigniter.checkArgumentTypeOfConfig% 107 | checkArgumentTypeOfModel: %codeigniter.checkArgumentTypeOfModel% 108 | 109 | - 110 | class: CodeIgniter\PHPStan\Rules\Functions\ServicesFunctionArgumentTypeRule 111 | 112 | conditionalTags: 113 | CodeIgniter\PHPStan\Rules\Functions\FactoriesFunctionArgumentTypeRule: 114 | phpstan.rules.rule: %codeigniter.checkArgumentTypeOfFactories% 115 | CodeIgniter\PHPStan\Rules\Functions\ServicesFunctionArgumentTypeRule: 116 | phpstan.rules.rule: %codeigniter.checkArgumentTypeOfServices% 117 | 118 | rules: 119 | - CodeIgniter\PHPStan\Rules\Classes\CacheHandlerInstantiationRule 120 | - CodeIgniter\PHPStan\Rules\Classes\FrameworkExceptionInstantiationRule 121 | - CodeIgniter\PHPStan\Rules\Functions\NoClassConstFetchOnFactoriesFunctions 122 | - CodeIgniter\PHPStan\Rules\Superglobals\SuperglobalAccessRule 123 | - CodeIgniter\PHPStan\Rules\Superglobals\SuperglobalAssignRule 124 | -------------------------------------------------------------------------------- /src/NodeVisitor/ModelReturnTypeTransformVisitor.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\NodeVisitor; 15 | 16 | use PhpParser\Node; 17 | use PhpParser\Node\Expr\MethodCall; 18 | use PhpParser\Node\Identifier; 19 | use PhpParser\Node\Scalar; 20 | use PhpParser\NodeVisitorAbstract; 21 | 22 | final class ModelReturnTypeTransformVisitor extends NodeVisitorAbstract 23 | { 24 | public const RETURN_TYPE = 'returnType'; 25 | 26 | /** 27 | * @var list 28 | */ 29 | private const RETURN_TYPE_GETTERS = ['find', 'findAll', 'first']; 30 | 31 | /** 32 | * @var list 33 | */ 34 | private const RETURN_TYPE_TRANSFORMERS = ['asArray', 'asObject']; 35 | 36 | /** 37 | * @return null 38 | */ 39 | public function enterNode(Node $node) 40 | { 41 | if (! $node instanceof MethodCall) { 42 | return null; 43 | } 44 | 45 | if (! $node->name instanceof Identifier) { 46 | return null; 47 | } 48 | 49 | if (! in_array($node->name->name, self::RETURN_TYPE_GETTERS, true)) { 50 | return null; 51 | } 52 | 53 | $lastNode = $node; 54 | 55 | while ($node->var instanceof MethodCall) { 56 | $node = $node->var; 57 | 58 | if (! $node->name instanceof Identifier) { 59 | continue; 60 | } 61 | 62 | if (! in_array($node->name->name, self::RETURN_TYPE_TRANSFORMERS, true)) { 63 | continue; 64 | } 65 | 66 | if ($node->name->name === 'asArray') { 67 | $lastNode->setAttribute(self::RETURN_TYPE, new Scalar\String_('array')); 68 | break; 69 | } 70 | 71 | $args = $node->getArgs(); 72 | 73 | if ($args === []) { 74 | $lastNode->setAttribute(self::RETURN_TYPE, new Scalar\String_('object')); 75 | break; 76 | } 77 | 78 | $lastNode->setAttribute(self::RETURN_TYPE, $args[0]->value); 79 | break; 80 | } 81 | 82 | return null; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Rules/Classes/CacheHandlerInstantiationRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Rules\Classes; 15 | 16 | use CodeIgniter\Cache\CacheFactory; 17 | use CodeIgniter\Cache\CacheInterface; 18 | use CodeIgniter\Test\Mock\MockCache; 19 | use PhpParser\Node; 20 | use PHPStan\Analyser\Scope; 21 | use PHPStan\Rules\Rule; 22 | use PHPStan\Rules\RuleErrorBuilder; 23 | use PHPStan\Type\ObjectType; 24 | 25 | /** 26 | * @implements Rule 27 | */ 28 | final class CacheHandlerInstantiationRule implements Rule 29 | { 30 | public function getNodeType(): string 31 | { 32 | return Node\Expr\New_::class; 33 | } 34 | 35 | public function processNode(Node $node, Scope $scope): array 36 | { 37 | if (! $node->class instanceof Node\Name) { 38 | return []; 39 | } 40 | 41 | $objectType = new ObjectType((string) $node->class); 42 | $reflection = $objectType->getClassReflection(); 43 | 44 | if ($reflection === null) { 45 | return []; 46 | } 47 | 48 | if (! (new ObjectType(CacheInterface::class))->isSuperTypeOf($objectType)->yes()) { 49 | return []; 50 | } 51 | 52 | if ($reflection->getName() === MockCache::class) { 53 | return []; 54 | } 55 | 56 | if ($scope->isInClass() && $scope->getClassReflection()->getName() === CacheFactory::class) { 57 | return []; 58 | } 59 | 60 | return [ 61 | RuleErrorBuilder::message(sprintf( 62 | 'Calling new %s() directly is incomplete to get the cache instance.', 63 | $reflection->getNativeReflection()->getShortName(), 64 | )) 65 | ->tip('Use CacheFactory::getHandler() or the cache() function to get the cache instance instead.') 66 | ->identifier('codeigniter.cacheHandlerInstance') 67 | ->build(), 68 | ]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Rules/Classes/FrameworkExceptionInstantiationRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Rules\Classes; 15 | 16 | use CodeIgniter\Exceptions\FrameworkException; 17 | use PhpParser\Node; 18 | use PHPStan\Analyser\Scope; 19 | use PHPStan\Rules\Rule; 20 | use PHPStan\Rules\RuleErrorBuilder; 21 | use PHPStan\ShouldNotHappenException; 22 | use PHPStan\Type\ObjectType; 23 | 24 | /** 25 | * @implements Rule 26 | */ 27 | final class FrameworkExceptionInstantiationRule implements Rule 28 | { 29 | public function getNodeType(): string 30 | { 31 | return Node\Expr\New_::class; 32 | } 33 | 34 | public function processNode(Node $node, Scope $scope): array 35 | { 36 | $class = $node->class; 37 | 38 | if (! $class instanceof Node\Name) { 39 | return []; 40 | } 41 | 42 | $objectType = new ObjectType($class->toString()); 43 | 44 | if (! (new ObjectType(FrameworkException::class))->isSuperTypeOf($objectType)->yes()) { 45 | return []; 46 | } 47 | 48 | if ($objectType->getClassReflection() === null) { 49 | throw new ShouldNotHappenException(); 50 | } 51 | 52 | return [RuleErrorBuilder::message(sprintf( 53 | 'Instantiating %s using new is not allowed. Use one of its named constructors instead.', 54 | $objectType->getClassReflection()->getNativeReflection()->getShortName(), 55 | ))->identifier('codeigniter.frameworkExceptionInstance')->build()]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Rules/Functions/FactoriesFunctionArgumentTypeRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Rules\Functions; 15 | 16 | use CodeIgniter\Config\BaseConfig; 17 | use CodeIgniter\Model; 18 | use CodeIgniter\PHPStan\Type\FactoriesReturnTypeHelper; 19 | use PhpParser\Node; 20 | use PHPStan\Analyser\Scope; 21 | use PHPStan\Reflection\ParametersAcceptorSelector; 22 | use PHPStan\Reflection\ReflectionProvider; 23 | use PHPStan\Rules\Rule; 24 | use PHPStan\Rules\RuleErrorBuilder; 25 | use PHPStan\Type\ObjectType; 26 | use PHPStan\Type\VerbosityLevel; 27 | 28 | /** 29 | * @implements Rule 30 | */ 31 | final class FactoriesFunctionArgumentTypeRule implements Rule 32 | { 33 | /** 34 | * @var array 35 | */ 36 | private array $instanceofMap = [ 37 | 'config' => BaseConfig::class, 38 | 'model' => Model::class, 39 | ]; 40 | 41 | /** 42 | * @var array 43 | */ 44 | private array $argumentTypeCheck = []; 45 | 46 | public function __construct( 47 | private readonly ReflectionProvider $reflectionProvider, 48 | private readonly FactoriesReturnTypeHelper $factoriesReturnTypeHelper, 49 | bool $checkArgumentTypeOfConfig, 50 | bool $checkArgumentTypeOfModel, 51 | ) { 52 | $this->argumentTypeCheck = [ 53 | 'config' => $checkArgumentTypeOfConfig, 54 | 'model' => $checkArgumentTypeOfModel, 55 | ]; 56 | } 57 | 58 | public function getNodeType(): string 59 | { 60 | return Node\Expr\FuncCall::class; 61 | } 62 | 63 | public function processNode(Node $node, Scope $scope): array 64 | { 65 | if (! $node->name instanceof Node\Name) { 66 | return []; 67 | } 68 | 69 | $nameNode = $node->name; 70 | $function = $this->reflectionProvider->resolveFunctionName($nameNode, $scope); 71 | 72 | if (! in_array($function, ['config', 'model'], true)) { 73 | return []; 74 | } 75 | 76 | $args = $node->getArgs(); 77 | 78 | if ($args === []) { 79 | return []; 80 | } 81 | 82 | $nameType = $scope->getType($args[0]->value); 83 | 84 | if ($nameType->isString()->no()) { 85 | return []; // caught elsewhere 86 | } 87 | 88 | $returnType = $this->factoriesReturnTypeHelper->check($nameType, $function); 89 | 90 | $firstParameter = ParametersAcceptorSelector::selectFromArgs( 91 | $scope, 92 | $node->getArgs(), 93 | $this->reflectionProvider->getFunction($nameNode, $scope)->getVariants(), 94 | )->getParameters()[0]; 95 | 96 | if ($returnType->isNull()->yes()) { 97 | $addTip = static function (RuleErrorBuilder $ruleErrorBuilder) use ($nameType, $function): RuleErrorBuilder { 98 | foreach ($nameType->getConstantStrings() as $constantStringType) { 99 | $ruleErrorBuilder->addTip(sprintf( 100 | 'If %s is a valid class string, you can add its possible namespace(s) in codeigniter.additional%sNamespaces in your %%configurationFile%%.', 101 | $constantStringType->describe(VerbosityLevel::precise()), 102 | ucfirst($function), 103 | )); 104 | } 105 | 106 | return $ruleErrorBuilder; 107 | }; 108 | 109 | return [$addTip(RuleErrorBuilder::message(sprintf( 110 | 'Parameter #1 $%s of function %s expects a valid class string, %s given.', 111 | $firstParameter->getName(), 112 | $function, 113 | $nameType->describe(VerbosityLevel::precise()), 114 | )))->identifier(sprintf('codeigniter.%sArgumentType', $function))->build()]; 115 | } 116 | 117 | if (! (new ObjectType($this->instanceofMap[$function]))->isSuperTypeOf($returnType)->yes()) { 118 | if (! $this->argumentTypeCheck[$function]) { 119 | return []; 120 | } 121 | 122 | return [RuleErrorBuilder::message(sprintf( 123 | 'Argument #1 $%s (%s) passed to function %s does not extend %s.', 124 | $firstParameter->getName(), 125 | $nameType->describe(VerbosityLevel::precise()), 126 | $function, 127 | addcslashes($this->instanceofMap[$function], '\\'), 128 | ))->identifier(sprintf('codeigniter.%sArgumentInstanceof', $function))->build()]; 129 | } 130 | 131 | return []; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Rules/Functions/NoClassConstFetchOnFactoriesFunctions.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Rules\Functions; 15 | 16 | use CodeIgniter\PHPStan\Type\FactoriesReturnTypeHelper; 17 | use PhpParser\Node; 18 | use PHPStan\Analyser\Scope; 19 | use PHPStan\Reflection\ReflectionProvider; 20 | use PHPStan\Rules\Rule; 21 | use PHPStan\Rules\RuleErrorBuilder; 22 | use PHPUnit\Framework\TestCase; 23 | 24 | /** 25 | * @implements Rule 26 | */ 27 | final class NoClassConstFetchOnFactoriesFunctions implements Rule 28 | { 29 | /** 30 | * @var array 31 | */ 32 | private static array $namespaceMap = [ 33 | 'config' => 'Config', 34 | 'model' => 'App\\Models', 35 | ]; 36 | 37 | public function __construct( 38 | private readonly ReflectionProvider $reflectionProvider, 39 | private readonly FactoriesReturnTypeHelper $factoriesReturnTypeHelper, 40 | ) {} 41 | 42 | public function getNodeType(): string 43 | { 44 | return Node\Expr\FuncCall::class; 45 | } 46 | 47 | public function processNode(Node $node, Scope $scope): array 48 | { 49 | if (! $node->name instanceof Node\Name) { 50 | return []; 51 | } 52 | 53 | $nameNode = $node->name; 54 | $function = $this->reflectionProvider->resolveFunctionName($nameNode, $scope); 55 | 56 | if (! in_array($function, ['config', 'model'], true)) { 57 | return []; 58 | } 59 | 60 | $args = $node->getArgs(); 61 | 62 | if ($args === []) { 63 | return []; 64 | } 65 | 66 | $classConstFetch = $args[0]->value; 67 | 68 | if (! $classConstFetch instanceof Node\Expr\ClassConstFetch) { 69 | return []; 70 | } 71 | 72 | if (! $classConstFetch->class instanceof Node\Name) { 73 | return []; 74 | } 75 | 76 | if (! $classConstFetch->name instanceof Node\Identifier || $classConstFetch->name->name !== 'class') { 77 | return []; 78 | } 79 | 80 | if ($scope->isInClass()) { 81 | $classRef = $scope->getClassReflection(); 82 | 83 | if ( 84 | $this->reflectionProvider->hasClass(TestCase::class) 85 | && $classRef->isSubclassOfClass($this->reflectionProvider->getClass(TestCase::class)) 86 | ) { 87 | return []; // skip uses in test classes as tests are internal 88 | } 89 | } 90 | 91 | $returnType = $this->factoriesReturnTypeHelper->check($scope->getType($classConstFetch), $function); 92 | 93 | if ($returnType->isNull()->yes()) { 94 | return []; 95 | } 96 | 97 | $reflections = $returnType->getObjectClassReflections(); 98 | 99 | if ($reflections === []) { 100 | return []; 101 | } 102 | 103 | $reflection = $reflections[0]; 104 | 105 | if ($reflection->getNativeReflection()->getNamespaceName() === self::$namespaceMap[$function]) { 106 | return []; 107 | } 108 | 109 | $fileNamespace = $scope->getNamespace(); 110 | 111 | if ( 112 | $fileNamespace !== null 113 | && ((defined('APP_NAMESPACE') && str_starts_with($fileNamespace, APP_NAMESPACE)) 114 | || str_starts_with($fileNamespace, 'App\\')) 115 | ) { 116 | return []; 117 | } 118 | 119 | return [ 120 | RuleErrorBuilder::message(sprintf( 121 | 'Call to function %s with %s::class is discouraged.', 122 | $function, 123 | $reflection->getDisplayName(), 124 | ))->tip(sprintf( 125 | 'Use %s(\'%s\') instead to allow overriding.', 126 | $function, 127 | $reflection->getNativeReflection()->getShortName(), 128 | ))->identifier('codeigniter.factoriesClassConstFetch')->build(), 129 | ]; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Rules/Functions/ServicesFunctionArgumentTypeRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Rules\Functions; 15 | 16 | use CodeIgniter\PHPStan\Type\ServicesReturnTypeHelper; 17 | use PhpParser\Node; 18 | use PHPStan\Analyser\Scope; 19 | use PHPStan\Reflection\ClassReflection; 20 | use PHPStan\Reflection\ReflectionProvider; 21 | use PHPStan\Rules\Rule; 22 | use PHPStan\Rules\RuleErrorBuilder; 23 | use PHPStan\Type\MixedType; 24 | use PHPStan\Type\VerbosityLevel; 25 | 26 | /** 27 | * @implements Rule 28 | */ 29 | final class ServicesFunctionArgumentTypeRule implements Rule 30 | { 31 | public function __construct( 32 | private readonly ReflectionProvider $reflectionProvider, 33 | private readonly ServicesReturnTypeHelper $servicesReturnTypeHelper, 34 | ) {} 35 | 36 | public function getNodeType(): string 37 | { 38 | return Node\Expr\FuncCall::class; 39 | } 40 | 41 | public function processNode(Node $node, Scope $scope): array 42 | { 43 | if (! $node->name instanceof Node\Name) { 44 | return []; 45 | } 46 | 47 | $nameNode = $node->name; 48 | $function = $this->reflectionProvider->resolveFunctionName($nameNode, $scope); 49 | 50 | if (! in_array($function, ['service', 'single_service'], true)) { 51 | return []; 52 | } 53 | 54 | $args = $node->getArgs(); 55 | 56 | if ($args === []) { 57 | return []; 58 | } 59 | 60 | $nameType = $scope->getType($args[0]->value); 61 | 62 | if ($nameType->isString()->no()) { 63 | return []; // caught elsewhere 64 | } 65 | 66 | $returnType = $this->servicesReturnTypeHelper->check($nameType, $scope); 67 | 68 | if ($returnType->isObject()->yes()) { 69 | return []; 70 | } 71 | 72 | $name = $nameType->describe(VerbosityLevel::precise()); 73 | 74 | $trimmedName = trim($name, "'"); 75 | 76 | if (in_array(strtolower($trimmedName), ServicesReturnTypeHelper::IMPOSSIBLE_SERVICE_METHOD_NAMES, true)) { 77 | return [ 78 | RuleErrorBuilder::message(sprintf('The method %s is reserved for service location internals and cannot be used as a service method.', $name)) 79 | ->identifier('codeigniter.reservedServiceName') 80 | ->build(), 81 | ]; 82 | } 83 | 84 | if ($returnType->isNull()->maybe() && $returnType instanceof MixedType) { 85 | return [ 86 | RuleErrorBuilder::message(sprintf('Service method %s returns mixed.', $name)) 87 | ->tip('Perhaps you forgot to add a return type?') 88 | ->identifier('codeigniter.serviceMixedReturn') 89 | ->build(), 90 | ]; 91 | } 92 | 93 | $hasMethod = array_reduce( 94 | $this->servicesReturnTypeHelper->getServicesReflection(), 95 | static fn (bool $carry, ClassReflection $service): bool => $carry || $service->hasMethod($trimmedName), 96 | false, 97 | ); 98 | 99 | if (! $returnType->isNull()->yes() || $hasMethod) { 100 | return [RuleErrorBuilder::message(sprintf( 101 | 'Service method %s expected to return a service instance, got %s instead.', 102 | $name, 103 | $returnType->describe(VerbosityLevel::precise()), 104 | ))->identifier('codeigniter.serviceNonObjectReturn')->build()]; 105 | } 106 | 107 | $addTip = static function (RuleErrorBuilder $ruleErrorBuilder) use ($nameType): RuleErrorBuilder { 108 | foreach ($nameType->getConstantStrings() as $constantStringType) { 109 | $ruleErrorBuilder->addTip(sprintf( 110 | 'If %s is a valid service method, you can add its possible services factory class(es) ' . 111 | 'in codeigniter.additionalServices in your %%configurationFile%%.', 112 | $constantStringType->describe(VerbosityLevel::precise()), 113 | )); 114 | } 115 | 116 | return $ruleErrorBuilder; 117 | }; 118 | 119 | return [$addTip(RuleErrorBuilder::message(sprintf( 120 | 'Call to unknown service method %s.', 121 | $nameType->describe(VerbosityLevel::precise()), 122 | )))->identifier('codeigniter.unknownServiceMethod')->build()]; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Rules/Superglobals/SuperglobalAccessRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Rules\Superglobals; 15 | 16 | use PhpParser\Node; 17 | use PHPStan\Analyser\Scope; 18 | use PHPStan\Rules\IdentifierRuleError; 19 | use PHPStan\Rules\Rule; 20 | use PHPStan\Rules\RuleErrorBuilder; 21 | use PHPStan\Type\VerbosityLevel; 22 | 23 | /** 24 | * @implements Rule 25 | */ 26 | final class SuperglobalAccessRule implements Rule 27 | { 28 | public function __construct( 29 | private readonly SuperglobalRuleHelper $superglobalRuleHelper, 30 | ) {} 31 | 32 | public function getNodeType(): string 33 | { 34 | return Node\Expr\ArrayDimFetch::class; 35 | } 36 | 37 | /** 38 | * @return list 39 | */ 40 | public function processNode(Node $node, Scope $scope): array 41 | { 42 | if ($scope->isInExpressionAssign($node)) { 43 | return []; 44 | } 45 | 46 | if (! $node->var instanceof Node\Expr\Variable) { 47 | return []; 48 | } 49 | 50 | $name = $node->var->name; 51 | 52 | if (! is_string($name)) { 53 | return []; 54 | } 55 | 56 | if (! $this->superglobalRuleHelper->isHandledSuperglobal($name)) { 57 | return []; 58 | } 59 | 60 | if ($scope->getFunction() === null) { 61 | return []; // ignore uses in root level (not inside function or method) 62 | } 63 | 64 | if ($node->dim === null) { 65 | return []; 66 | } 67 | 68 | $dimType = $scope->getType($node->dim); 69 | 70 | if ($dimType->isString()->no()) { 71 | return []; 72 | } 73 | 74 | $method = $this->superglobalRuleHelper->getSuperglobalMethodGetter($name); 75 | 76 | if ($dimType->getConstantStrings() !== []) { 77 | $errors = []; 78 | 79 | foreach ($dimType->getConstantStrings() as $dimString) { 80 | $dim = $dimString->getValue(); 81 | 82 | if ($this->superglobalRuleHelper->isAllowedOffsetAccess($name, $dim)) { 83 | continue; 84 | } 85 | 86 | $errors[] = RuleErrorBuilder::message(sprintf('Accessing offset \'%s\' directly on $%s is discouraged.', $dim, $name)) 87 | ->tip(sprintf('Use \\Config\\Services::superglobals()->%s(\'%s\') instead.', $method, $dim)) 88 | ->identifier('codeigniter.superglobalAccess') 89 | ->build(); 90 | } 91 | 92 | return $errors; 93 | } 94 | 95 | $dim = $dimType->describe(VerbosityLevel::precise()); 96 | 97 | return [ 98 | RuleErrorBuilder::message(sprintf('Accessing offset %s directly on $%s is discouraged.', $dim, $name)) 99 | ->tip(sprintf('Use \\Config\\Services::superglobals()->%s() instead.', $method)) 100 | ->identifier('codeigniter.superglobalAccess') 101 | ->build(), 102 | ]; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Rules/Superglobals/SuperglobalAssignRule.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Rules\Superglobals; 15 | 16 | use CodeIgniter\Superglobals; 17 | use PhpParser\Node; 18 | use PHPStan\Analyser\Scope; 19 | use PHPStan\Rules\IdentifierRuleError; 20 | use PHPStan\Rules\Rule; 21 | use PHPStan\Rules\RuleErrorBuilder; 22 | use PHPStan\Type\VerbosityLevel; 23 | 24 | /** 25 | * @implements Rule 26 | */ 27 | final class SuperglobalAssignRule implements Rule 28 | { 29 | public function __construct( 30 | private readonly SuperglobalRuleHelper $superglobalRuleHelper, 31 | ) {} 32 | 33 | public function getNodeType(): string 34 | { 35 | return Node\Expr\Assign::class; 36 | } 37 | 38 | public function processNode(Node $node, Scope $scope): array 39 | { 40 | if ($node->var instanceof Node\Expr\ArrayDimFetch) { 41 | return $this->processArrayDimFetch($node, $scope); 42 | } 43 | 44 | if ($node->var instanceof Node\Expr\Variable) { 45 | return $this->processVariableExpr($node, $scope); 46 | } 47 | 48 | return []; 49 | } 50 | 51 | /** 52 | * @param Node\Expr\Assign $node 53 | * 54 | * @return list 55 | */ 56 | private function processArrayDimFetch(Node $node, Scope $scope): array 57 | { 58 | assert($node->var instanceof Node\Expr\ArrayDimFetch); 59 | 60 | $arrayDimFetch = $node->var; 61 | 62 | if ($arrayDimFetch->dim === null) { 63 | return []; 64 | } 65 | 66 | $dimType = $scope->getType($arrayDimFetch->dim); 67 | 68 | if ($dimType->isString()->no()) { 69 | return []; 70 | } 71 | 72 | if (! $arrayDimFetch->var instanceof Node\Expr\Variable) { 73 | return []; 74 | } 75 | 76 | $name = $arrayDimFetch->var->name; 77 | 78 | if (! is_string($name)) { 79 | return []; 80 | } 81 | 82 | if (! $this->superglobalRuleHelper->isHandledSuperglobal($name)) { 83 | return []; 84 | } 85 | 86 | if ($scope->getFunction() === null) { 87 | return []; // ignore uses in root level (not inside function or method) 88 | } 89 | 90 | if ($scope->isInClass() && $scope->getClassReflection()->getName() === Superglobals::class) { 91 | return []; 92 | } 93 | 94 | $exprType = $scope->getType($node->expr); 95 | 96 | $method = $this->superglobalRuleHelper->getSuperglobalMethodSetter($name); 97 | 98 | if ($dimType->getConstantStrings() !== []) { 99 | $errors = []; 100 | 101 | foreach ($dimType->getConstantStrings() as $dimString) { 102 | $dim = $dimString->getValue(); 103 | 104 | if ($this->superglobalRuleHelper->isAllowedOffsetAccess($name, $dim)) { 105 | continue; 106 | } 107 | 108 | $expr = $exprType->describe(VerbosityLevel::precise()); 109 | 110 | $errors[] = RuleErrorBuilder::message(sprintf('Assigning %s directly on offset \'%s\' of $%s is discouraged.', $expr, $dim, $name)) 111 | ->identifier('codeigniter.superglobalAccessAssign') 112 | ->tip(sprintf('Use \\Config\\Services::superglobals()->%s(\'%s\', %s) instead.', $method, $dim, $expr)) 113 | ->build(); 114 | } 115 | 116 | return $errors; 117 | } 118 | 119 | return [ 120 | RuleErrorBuilder::message(sprintf( 121 | 'Assigning %s directly on offset %s of $%s is discouraged.', 122 | $exprType->describe(VerbosityLevel::precise()), 123 | $dimType->describe(VerbosityLevel::precise()), 124 | $name, 125 | )) 126 | ->identifier('codeigniter.superglobalAccessAssign') 127 | ->tip(sprintf('Use \\Config\\Services::superglobals()->%s(...) instead.', $method)) 128 | ->build(), 129 | ]; 130 | } 131 | 132 | /** 133 | * @param Node\Expr\Assign $node 134 | * 135 | * @return list 136 | */ 137 | private function processVariableExpr(Node $node, Scope $scope): array 138 | { 139 | assert($node->var instanceof Node\Expr\Variable); 140 | 141 | $name = $node->var->name; 142 | 143 | if (! is_string($name)) { 144 | return []; 145 | } 146 | 147 | if (! $this->superglobalRuleHelper->isHandledSuperglobal($name)) { 148 | return []; 149 | } 150 | 151 | if ($name !== '_GET') { 152 | return []; 153 | } 154 | 155 | if ($scope->isInClass() && $scope->getClassReflection()->getName() === Superglobals::class) { 156 | return []; 157 | } 158 | 159 | $exprType = $scope->getType($node->expr); 160 | 161 | if (! $exprType->isArray()->yes()) { 162 | return [ 163 | RuleErrorBuilder::message(sprintf('Cannot re-assign non-arrays to $_GET, got %s.', $exprType->describe(VerbosityLevel::typeOnly()))) 164 | ->identifier('codeigniter.getReassignNonarray') 165 | ->build(), 166 | ]; 167 | } 168 | 169 | if ($scope->getFunction() === null) { 170 | return []; // ignore uses in root level (not inside function or method) 171 | } 172 | 173 | return [ 174 | RuleErrorBuilder::message('Re-assigning arrays to $_GET directly is discouraged.') 175 | ->tip('Use \\Config\\Services::superglobals()->setGetArray() instead.') 176 | ->identifier('codeigniter.getReassignArray') 177 | ->build(), 178 | ]; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Rules/Superglobals/SuperglobalRuleHelper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Rules\Superglobals; 15 | 16 | use InvalidArgumentException; 17 | 18 | final class SuperglobalRuleHelper 19 | { 20 | /** 21 | * @var array{_SERVER: list, _GET: list} 22 | */ 23 | private static array $allowedOffsetAccess = [ 24 | '_SERVER' => ['argv', 'argc', 'REQUEST_TIME', 'REQUEST_TIME_FLOAT'], 25 | '_GET' => [], 26 | ]; 27 | 28 | public function isHandledSuperglobal(string $name): bool 29 | { 30 | return in_array($name, ['_SERVER', '_GET'], true); 31 | } 32 | 33 | public function isAllowedOffsetAccess(string $name, string $offset): bool 34 | { 35 | if (! $this->isHandledSuperglobal($name)) { 36 | return false; 37 | } 38 | 39 | return in_array($offset, self::$allowedOffsetAccess[$name], true); 40 | } 41 | 42 | /** 43 | * @throws InvalidArgumentException 44 | */ 45 | public function getSuperglobalMethodSetter(string $name): string 46 | { 47 | return match ($name) { 48 | '_SERVER' => 'setServer', 49 | '_GET' => 'setGet', 50 | default => throw new InvalidArgumentException(sprintf('Unhandled superglobal: "%s".', $name)), 51 | }; 52 | } 53 | 54 | /** 55 | * @throws InvalidArgumentException 56 | */ 57 | public function getSuperglobalMethodGetter(string $name): string 58 | { 59 | return match ($name) { 60 | '_SERVER' => 'server', 61 | '_GET' => 'get', 62 | default => throw new InvalidArgumentException(sprintf('Unhandled superglobal: "%s".', $name)), 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Type/CacheFactoryGetHandlerReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Type; 15 | 16 | use CodeIgniter\Cache\CacheFactory; 17 | use CodeIgniter\Cache\CacheInterface; 18 | use CodeIgniter\Cache\Handlers\BaseHandler; 19 | use Config\Cache; 20 | use PhpParser\Node\Expr; 21 | use PhpParser\Node\Expr\ConstFetch; 22 | use PhpParser\Node\Expr\StaticCall; 23 | use PhpParser\Node\Name; 24 | use PHPStan\Analyser\Scope; 25 | use PHPStan\Reflection\MethodReflection; 26 | use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; 27 | use PHPStan\Type\IntersectionType; 28 | use PHPStan\Type\NeverType; 29 | use PHPStan\Type\ObjectType; 30 | use PHPStan\Type\Type; 31 | use PHPStan\Type\TypeCombinator; 32 | use PHPStan\Type\TypeTraverser; 33 | use PHPStan\Type\TypeUtils; 34 | use PHPStan\Type\UnionType; 35 | 36 | final class CacheFactoryGetHandlerReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension 37 | { 38 | public function __construct( 39 | private readonly bool $addBackupHandlerAsReturnType, 40 | ) {} 41 | 42 | public function getClass(): string 43 | { 44 | return CacheFactory::class; 45 | } 46 | 47 | public function isStaticMethodSupported(MethodReflection $methodReflection): bool 48 | { 49 | return $methodReflection->getName() === 'getHandler'; 50 | } 51 | 52 | public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type 53 | { 54 | $args = $methodCall->getArgs(); 55 | 56 | if ($args === []) { 57 | return null; 58 | } 59 | 60 | $cache = $this->getCache($args[0]->value, $scope); 61 | 62 | if ($cache === null || $cache->validHandlers === []) { 63 | return new NeverType(true); 64 | } 65 | 66 | $handlerType = $this->getHandlerType( 67 | $args[1]->value ?? new ConstFetch(new Name('null')), 68 | $scope, 69 | $cache->validHandlers, 70 | $cache->handler, 71 | ); 72 | $backupHandlerType = $this->getHandlerType( 73 | $args[2]->value ?? new ConstFetch(new Name('null')), 74 | $scope, 75 | $cache->validHandlers, 76 | $cache->backupHandler, 77 | ); 78 | 79 | if (! $handlerType->isObject()->yes()) { 80 | return $backupHandlerType; 81 | } 82 | 83 | if (! $this->addBackupHandlerAsReturnType) { 84 | return $handlerType; 85 | } 86 | 87 | return TypeUtils::toBenevolentUnion(TypeCombinator::union($handlerType, $backupHandlerType)); 88 | } 89 | 90 | private function getCache(Expr $expr, Scope $scope): ?Cache 91 | { 92 | foreach ($scope->getType($expr)->getObjectClassReflections() as $classReflection) { 93 | if ($classReflection->getName() === Cache::class) { 94 | $cache = $classReflection->getNativeReflection()->newInstance(); 95 | 96 | if ($cache instanceof Cache) { 97 | return $cache; 98 | } 99 | } 100 | } 101 | 102 | return null; 103 | } 104 | 105 | /** 106 | * @param array> $validHandlers 107 | */ 108 | private function getHandlerType(Expr $expr, Scope $scope, array $validHandlers, string $default): Type 109 | { 110 | return TypeTraverser::map( 111 | $scope->getType($expr), 112 | static function (Type $type, callable $traverse) use ($validHandlers, $default): Type { 113 | if ($type instanceof UnionType || $type instanceof IntersectionType) { 114 | return $traverse($type); 115 | } 116 | 117 | if ($type->isNull()->yes()) { 118 | if (! isset($validHandlers[$default])) { 119 | return new NeverType(true); 120 | } 121 | 122 | return new ObjectType($validHandlers[$default]); 123 | } 124 | 125 | $types = []; 126 | 127 | foreach ($type->getConstantStrings() as $constantString) { 128 | $name = $constantString->getValue(); 129 | 130 | if (isset($validHandlers[$name])) { 131 | $types[] = new ObjectType($validHandlers[$name]); 132 | } else { 133 | $types[] = new NeverType(true); 134 | } 135 | } 136 | 137 | if ($types === []) { 138 | return new ObjectType(BaseHandler::class); 139 | } 140 | 141 | return TypeCombinator::union(...$types); 142 | }, 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Type/FactoriesFunctionReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Type; 15 | 16 | use PhpParser\Node\Expr\FuncCall; 17 | use PHPStan\Analyser\Scope; 18 | use PHPStan\Reflection\FunctionReflection; 19 | use PHPStan\Type\DynamicFunctionReturnTypeExtension; 20 | use PHPStan\Type\Type; 21 | 22 | final class FactoriesFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension 23 | { 24 | public function __construct( 25 | private readonly FactoriesReturnTypeHelper $factoriesReturnTypeHelper, 26 | ) {} 27 | 28 | public function isFunctionSupported(FunctionReflection $functionReflection): bool 29 | { 30 | return in_array($functionReflection->getName(), ['config', 'model'], true); 31 | } 32 | 33 | public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type 34 | { 35 | $arguments = $functionCall->getArgs(); 36 | 37 | if ($arguments === []) { 38 | return null; 39 | } 40 | 41 | return $this->factoriesReturnTypeHelper->check( 42 | $scope->getType($arguments[0]->value), 43 | $functionReflection->getName(), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Type/FactoriesReturnTypeHelper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Type; 15 | 16 | use PHPStan\Reflection\ReflectionProvider; 17 | use PHPStan\Type\IntersectionType; 18 | use PHPStan\Type\NullType; 19 | use PHPStan\Type\ObjectType; 20 | use PHPStan\Type\Type; 21 | use PHPStan\Type\TypeTraverser; 22 | use PHPStan\Type\UnionType; 23 | 24 | final class FactoriesReturnTypeHelper 25 | { 26 | /** 27 | * @var array 28 | */ 29 | private array $namespaceMap = [ 30 | 'config' => 'Config\\', 31 | 'model' => 'App\\Models\\', 32 | ]; 33 | 34 | /** 35 | * @var array> 36 | */ 37 | private array $additionalNamespacesMap = [ 38 | 'config' => [], 39 | 'model' => [], 40 | ]; 41 | 42 | /** 43 | * @param array $additionalConfigNamespaces 44 | * @param array $additionalModelNamespaces 45 | */ 46 | public function __construct( 47 | private readonly ReflectionProvider $reflectionProvider, 48 | array $additionalConfigNamespaces, 49 | array $additionalModelNamespaces, 50 | ) { 51 | $cb = static fn (string $item): string => rtrim($item, '\\') . '\\'; 52 | 53 | $this->additionalNamespacesMap = [ 54 | 'config' => [...$this->additionalNamespacesMap['config'], ...array_map($cb, $additionalConfigNamespaces)], 55 | 'model' => [...$this->additionalNamespacesMap['model'], ...array_map($cb, $additionalModelNamespaces)], 56 | ]; 57 | } 58 | 59 | public function check(Type $type, string $function): Type 60 | { 61 | return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($function): Type { 62 | if ($type instanceof UnionType || $type instanceof IntersectionType) { 63 | return $traverse($type); 64 | } 65 | 66 | if ($type->isClassString()->yes()) { 67 | return $type->getClassStringObjectType(); 68 | } 69 | 70 | foreach ($type->getConstantStrings() as $constantStringType) { 71 | if ($constantStringType->isClassString()->yes()) { 72 | return $constantStringType->getClassStringObjectType(); 73 | } 74 | 75 | $constantString = $constantStringType->getValue(); 76 | 77 | $appName = $this->namespaceMap[$function] . $constantString; 78 | 79 | if ($this->reflectionProvider->hasClass($appName)) { 80 | return new ObjectType($appName); 81 | } 82 | 83 | foreach ($this->additionalNamespacesMap[$function] as $additionalNamespace) { 84 | $moduleClassName = $additionalNamespace . $constantString; 85 | 86 | if ($this->reflectionProvider->hasClass($moduleClassName)) { 87 | return new ObjectType($moduleClassName); 88 | } 89 | } 90 | } 91 | 92 | return new NullType(); 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Type/FakeFunctionReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Type; 15 | 16 | use PhpParser\Node\Expr\FuncCall; 17 | use PHPStan\Analyser\Scope; 18 | use PHPStan\Reflection\FunctionReflection; 19 | use PHPStan\Type\DynamicFunctionReturnTypeExtension; 20 | use PHPStan\Type\NonAcceptingNeverType; 21 | use PHPStan\Type\Type; 22 | 23 | final class FakeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension 24 | { 25 | public function __construct( 26 | private readonly ModelFetchedReturnTypeHelper $modelFetchedReturnTypeHelper, 27 | private readonly FactoriesReturnTypeHelper $factoriesReturnTypeHelper, 28 | ) {} 29 | 30 | public function isFunctionSupported(FunctionReflection $functionReflection): bool 31 | { 32 | return $functionReflection->getName() === 'fake'; 33 | } 34 | 35 | public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type 36 | { 37 | $arguments = $functionCall->getArgs(); 38 | 39 | if ($arguments === []) { 40 | return null; 41 | } 42 | 43 | $modelType = $this->factoriesReturnTypeHelper->check($scope->getType($arguments[0]->value), 'model'); 44 | 45 | if (! $modelType->isObject()->yes()) { 46 | return new NonAcceptingNeverType(); 47 | } 48 | 49 | $classReflections = $modelType->getObjectClassReflections(); 50 | 51 | if (count($classReflections) !== 1) { 52 | return $modelType; // ObjectWithoutClassType 53 | } 54 | 55 | $classReflection = current($classReflections); 56 | 57 | return $this->modelFetchedReturnTypeHelper->getFetchedReturnType($classReflection, null, $scope); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Type/ModelFetchedReturnTypeHelper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Type; 15 | 16 | use CodeIgniter\PHPStan\NodeVisitor\ModelReturnTypeTransformVisitor; 17 | use PhpParser\Node\Expr; 18 | use PhpParser\Node\Expr\MethodCall; 19 | use PHPStan\Analyser\Scope; 20 | use PHPStan\Reflection\ClassReflection; 21 | use PHPStan\Reflection\ReflectionProvider; 22 | use PHPStan\ShouldNotHappenException; 23 | use PHPStan\Type\BooleanType; 24 | use PHPStan\Type\Constant\ConstantArrayType; 25 | use PHPStan\Type\Constant\ConstantStringType; 26 | use PHPStan\Type\IntegerType; 27 | use PHPStan\Type\ObjectType; 28 | use PHPStan\Type\ObjectWithoutClassType; 29 | use PHPStan\Type\StringType; 30 | use PHPStan\Type\Type; 31 | use stdClass; 32 | 33 | final class ModelFetchedReturnTypeHelper 34 | { 35 | /** 36 | * @var array> 37 | */ 38 | private static array $notStringFormattedFields = [ 39 | 'active' => BooleanType::class, 40 | 'force_reset' => BooleanType::class, 41 | 'id' => IntegerType::class, 42 | 'success' => BooleanType::class, 43 | 'user_id' => IntegerType::class, 44 | ]; 45 | 46 | /** 47 | * @var array> 48 | */ 49 | private static array $typeInterpolations = [ 50 | 'bool' => BooleanType::class, 51 | 'int' => IntegerType::class, 52 | ]; 53 | 54 | /** 55 | * @var list 56 | */ 57 | private array $dateFields = []; 58 | 59 | /** 60 | * @param array $notStringFormattedFieldsArray 61 | */ 62 | public function __construct( 63 | private readonly ReflectionProvider $reflectionProvider, 64 | array $notStringFormattedFieldsArray, 65 | ) { 66 | foreach ($notStringFormattedFieldsArray as $field => $type) { 67 | if (! isset(self::$typeInterpolations[$type])) { 68 | continue; 69 | } 70 | 71 | self::$notStringFormattedFields[$field] = self::$typeInterpolations[$type]; 72 | } 73 | } 74 | 75 | public function getFetchedReturnType(ClassReflection $classReflection, ?MethodCall $methodCall, Scope $scope): Type 76 | { 77 | $returnType = $this->getNativeStringPropertyValue($classReflection, $scope, ModelReturnTypeTransformVisitor::RETURN_TYPE); 78 | 79 | if ($methodCall !== null && $methodCall->hasAttribute(ModelReturnTypeTransformVisitor::RETURN_TYPE)) { 80 | /** @var Expr $returnExpr */ 81 | $returnExpr = $methodCall->getAttribute(ModelReturnTypeTransformVisitor::RETURN_TYPE); 82 | $returnType = $this->getStringValueFromExpr($returnExpr, $scope); 83 | } 84 | 85 | if ($returnType === 'object') { 86 | return new ObjectType(stdClass::class); 87 | } 88 | 89 | if ($returnType === 'array') { 90 | return $this->getArrayReturnType($classReflection, $scope); 91 | } 92 | 93 | if ($this->reflectionProvider->hasClass($returnType)) { 94 | return new ObjectType($returnType); 95 | } 96 | 97 | return new ObjectWithoutClassType(); 98 | } 99 | 100 | private function getArrayReturnType(ClassReflection $classReflection, Scope $scope): Type 101 | { 102 | $this->fillDateFields($classReflection, $scope); 103 | $fieldsTypes = $scope->getType( 104 | $classReflection->getNativeProperty('allowedFields')->getNativeReflection()->getDefaultValueExpression(), 105 | )->getConstantArrays(); 106 | 107 | if ($fieldsTypes === []) { 108 | return new ConstantArrayType([], []); 109 | } 110 | 111 | $fields = array_filter( 112 | array_map( 113 | static fn (Type $type) => current($type->getConstantStrings()), 114 | current($fieldsTypes)->getValueTypes(), 115 | ), 116 | static fn (ConstantStringType|false $constantStringType): bool => $constantStringType !== false, 117 | ); 118 | 119 | return new ConstantArrayType( 120 | $fields, 121 | array_map(function (ConstantStringType $fieldType) use ($classReflection, $scope): Type { 122 | $field = $fieldType->getValue(); 123 | 124 | if (array_key_exists($field, self::$notStringFormattedFields)) { 125 | $type = self::$notStringFormattedFields[$field]; 126 | 127 | return new $type(); 128 | } 129 | 130 | if ( 131 | in_array($field, $this->dateFields, true) 132 | && $this->getNativeStringPropertyValue($classReflection, $scope, 'dateFormat') === 'int' 133 | ) { 134 | return new IntegerType(); 135 | } 136 | 137 | return new StringType(); 138 | }, $fields), 139 | ); 140 | } 141 | 142 | private function fillDateFields(ClassReflection $classReflection, Scope $scope): void 143 | { 144 | foreach (['createdAt', 'updatedAt', 'deletedAt'] as $property) { 145 | if ($classReflection->hasNativeProperty($property)) { 146 | $this->dateFields[] = $this->getNativeStringPropertyValue($classReflection, $scope, $property); 147 | } 148 | } 149 | } 150 | 151 | private function getNativeStringPropertyValue(ClassReflection $classReflection, Scope $scope, string $property): string 152 | { 153 | if (! $classReflection->hasNativeProperty($property)) { 154 | throw new ShouldNotHappenException(sprintf('Native property %s::$%s does not exist.', $classReflection->getDisplayName(), $property)); 155 | } 156 | 157 | return $this->getStringValueFromExpr( 158 | $classReflection->getNativeProperty($property)->getNativeReflection()->getDefaultValueExpression(), 159 | $scope, 160 | ); 161 | } 162 | 163 | private function getStringValueFromExpr(Expr $expr, Scope $scope): string 164 | { 165 | $exprType = $scope->getType($expr)->getConstantStrings(); 166 | assert(count($exprType) === 1); 167 | 168 | return current($exprType)->getValue(); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Type/ModelFindReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Type; 15 | 16 | use CodeIgniter\Model; 17 | use PhpParser\Node\Expr\MethodCall; 18 | use PHPStan\Analyser\Scope; 19 | use PHPStan\Reflection\ClassReflection; 20 | use PHPStan\Reflection\MethodReflection; 21 | use PHPStan\Type\Accessory\AccessoryArrayListType; 22 | use PHPStan\Type\ArrayType; 23 | use PHPStan\Type\Constant\ConstantArrayType; 24 | use PHPStan\Type\DynamicMethodReturnTypeExtension; 25 | use PHPStan\Type\IntegerType; 26 | use PHPStan\Type\IntersectionType; 27 | use PHPStan\Type\Type; 28 | use PHPStan\Type\TypeCombinator; 29 | use PHPStan\Type\TypeTraverser; 30 | use PHPStan\Type\UnionType; 31 | 32 | final class ModelFindReturnTypeExtension implements DynamicMethodReturnTypeExtension 33 | { 34 | public function __construct( 35 | private readonly ModelFetchedReturnTypeHelper $modelFetchedReturnTypeHelper, 36 | ) {} 37 | 38 | public function getClass(): string 39 | { 40 | return Model::class; 41 | } 42 | 43 | public function isMethodSupported(MethodReflection $methodReflection): bool 44 | { 45 | return in_array($methodReflection->getName(), ['find', 'findAll', 'first'], true); 46 | } 47 | 48 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type 49 | { 50 | $methodName = $methodReflection->getName(); 51 | 52 | if ($methodName === 'find') { 53 | return $this->getTypeFromFind($methodCall, $scope); 54 | } 55 | 56 | if ($methodName === 'findAll') { 57 | return $this->getTypeFromFindAll($methodCall, $scope); 58 | } 59 | 60 | $classReflection = $this->getClassReflection($methodCall, $scope); 61 | 62 | return TypeCombinator::addNull($this->modelFetchedReturnTypeHelper->getFetchedReturnType($classReflection, $methodCall, $scope)); 63 | } 64 | 65 | private function getClassReflection(MethodCall $methodCall, Scope $scope): ClassReflection 66 | { 67 | $classTypes = $scope->getType($methodCall->var)->getObjectClassReflections(); 68 | assert(count($classTypes) === 1); 69 | 70 | return current($classTypes); 71 | } 72 | 73 | private function getTypeFromFind(MethodCall $methodCall, Scope $scope): Type 74 | { 75 | $args = $methodCall->getArgs(); 76 | 77 | if (! isset($args[0])) { 78 | return $this->getTypeFromFindAll($methodCall, $scope); 79 | } 80 | 81 | return TypeTraverser::map( 82 | $scope->getType($args[0]->value), 83 | function (Type $idType, callable $traverse) use ($methodCall, $scope): Type { 84 | if ($idType instanceof UnionType || $idType instanceof IntersectionType) { 85 | return $traverse($idType); 86 | } 87 | 88 | if ($idType->isArray()->yes() && ! $idType->isIterableAtLeastOnce()->yes()) { 89 | return new ConstantArrayType([], []); 90 | } 91 | 92 | if ($idType->isInteger()->yes() || $idType->isString()->yes()) { 93 | $classReflection = $this->getClassReflection($methodCall, $scope); 94 | 95 | return TypeCombinator::addNull($this->modelFetchedReturnTypeHelper->getFetchedReturnType($classReflection, $methodCall, $scope)); 96 | } 97 | 98 | return $this->getTypeFromFindAll($methodCall, $scope); 99 | }, 100 | ); 101 | } 102 | 103 | private function getTypeFromFindAll(MethodCall $methodCall, Scope $scope): Type 104 | { 105 | $classReflection = $this->getClassReflection($methodCall, $scope); 106 | 107 | return TypeCombinator::intersect( 108 | new ArrayType( 109 | new IntegerType(), 110 | $this->modelFetchedReturnTypeHelper->getFetchedReturnType($classReflection, $methodCall, $scope), 111 | ), 112 | new AccessoryArrayListType(), 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Type/ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Type; 15 | 16 | use PhpParser\Node\Expr\StaticCall; 17 | use PHPStan\Analyser\Scope; 18 | use PHPStan\Reflection\MethodReflection; 19 | use PHPStan\Reflection\ParametersAcceptorSelector; 20 | use PHPStan\Type\ClosureType; 21 | use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; 22 | use PHPStan\Type\IntersectionType; 23 | use PHPStan\Type\NeverType; 24 | use PHPStan\Type\Type; 25 | use PHPStan\Type\TypeCombinator; 26 | use PHPStan\Type\TypeTraverser; 27 | use PHPStan\Type\UnionType; 28 | 29 | final class ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension 30 | { 31 | private const OBJECT_AS_STRING_CONTEXT = 0; 32 | private const OBJECT_AS_OBJECT_CONTEXT = 1; 33 | 34 | /** 35 | * @param class-string $class 36 | */ 37 | public function __construct( 38 | private readonly string $class, 39 | ) {} 40 | 41 | public function getClass(): string 42 | { 43 | return $this->class; 44 | } 45 | 46 | public function isStaticMethodSupported(MethodReflection $methodReflection): bool 47 | { 48 | return $methodReflection->getName() === 'getPrivateMethodInvoker'; 49 | } 50 | 51 | public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type 52 | { 53 | $args = $methodCall->getArgs(); 54 | 55 | if (count($args) !== 2) { 56 | return null; 57 | } 58 | 59 | $objectType = $scope->getType($args[0]->value); 60 | $methodType = $scope->getType($args[1]->value); 61 | 62 | return TypeTraverser::map($objectType, static function (Type $type, callable $traverse) use ($methodType, $scope, $args, $methodReflection): Type { 63 | if ($type instanceof UnionType || $type instanceof IntersectionType) { 64 | return $traverse($type); 65 | } 66 | 67 | $context = self::OBJECT_AS_OBJECT_CONTEXT; 68 | 69 | if ($type->isString()->yes()) { 70 | $context = self::OBJECT_AS_STRING_CONTEXT; 71 | } 72 | 73 | $closures = []; 74 | 75 | $objectType = $type->getObjectTypeOrClassStringObjectType(); 76 | 77 | foreach ($objectType->getObjectClassReflections() as $classReflection) { 78 | foreach ($methodType->getConstantStrings() as $methodStringType) { 79 | $methodName = $methodStringType->getValue(); 80 | 81 | if (! $classReflection->hasMethod($methodName)) { 82 | $closures[] = new NeverType(true); 83 | 84 | continue; 85 | } 86 | 87 | $invokedMethodReflection = $classReflection->getMethod($methodName, $scope); 88 | 89 | $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( 90 | $scope, 91 | [], 92 | $invokedMethodReflection->getVariants(), 93 | $invokedMethodReflection->getNamedArgumentsVariants(), 94 | ); 95 | 96 | if (! $invokedMethodReflection->isStatic() && $context === self::OBJECT_AS_STRING_CONTEXT) { 97 | // ReflectionException: Trying to invoke non static method FQCN::method() without an object 98 | $returnType = new NeverType(true); 99 | } elseif (strtolower($methodName) === '__construct') { 100 | // Do not use void as the return type of __construct 101 | $returnType = $objectType; 102 | } else { 103 | $returnType = $parametersAcceptor->getReturnType(); 104 | } 105 | 106 | $closures[] = new ClosureType( 107 | $parametersAcceptor->getParameters(), 108 | $returnType, 109 | $parametersAcceptor->isVariadic(), 110 | $parametersAcceptor->getTemplateTypeMap(), 111 | $parametersAcceptor->getResolvedTemplateTypeMap(), 112 | ); 113 | } 114 | } 115 | 116 | if ($closures === []) { 117 | if (! $objectType->isObject()->yes()) { 118 | return new NeverType(true); 119 | } 120 | 121 | return ParametersAcceptorSelector::selectFromArgs( 122 | $scope, 123 | $args, 124 | $methodReflection->getVariants(), 125 | )->getReturnType(); 126 | } 127 | 128 | return TypeCombinator::union(...$closures); 129 | }); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Type/ServicesFunctionReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Type; 15 | 16 | use PhpParser\Node\Expr\FuncCall; 17 | use PHPStan\Analyser\Scope; 18 | use PHPStan\Reflection\FunctionReflection; 19 | use PHPStan\Type\DynamicFunctionReturnTypeExtension; 20 | use PHPStan\Type\Type; 21 | 22 | final class ServicesFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension 23 | { 24 | public function __construct( 25 | private readonly ServicesReturnTypeHelper $servicesReturnTypeHelper, 26 | ) {} 27 | 28 | public function isFunctionSupported(FunctionReflection $functionReflection): bool 29 | { 30 | return in_array($functionReflection->getName(), ['service', 'single_service'], true); 31 | } 32 | 33 | public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type 34 | { 35 | $arguments = $functionCall->getArgs(); 36 | 37 | if ($arguments === []) { 38 | return null; 39 | } 40 | 41 | return $this->servicesReturnTypeHelper->check($scope->getType($arguments[0]->value), $scope); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Type/ServicesGetSharedInstanceReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Type; 15 | 16 | use CodeIgniter\Config\BaseService; 17 | use PhpParser\Node\Expr\StaticCall; 18 | use PHPStan\Analyser\Scope; 19 | use PHPStan\Reflection\MethodReflection; 20 | use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; 21 | use PHPStan\Type\Type; 22 | 23 | class ServicesGetSharedInstanceReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension 24 | { 25 | public function __construct( 26 | private readonly ServicesReturnTypeHelper $servicesReturnTypeHelper, 27 | ) {} 28 | 29 | public function getClass(): string 30 | { 31 | return BaseService::class; 32 | } 33 | 34 | public function isStaticMethodSupported(MethodReflection $methodReflection): bool 35 | { 36 | return $methodReflection->getName() === 'getSharedInstance'; 37 | } 38 | 39 | public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type 40 | { 41 | $arguments = $methodCall->getArgs(); 42 | 43 | if ($arguments === []) { 44 | return null; 45 | } 46 | 47 | return $this->servicesReturnTypeHelper->check($scope->getType($arguments[0]->value), $scope); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Type/ServicesReturnTypeHelper.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view 11 | * the LICENSE file that was distributed with this source code. 12 | */ 13 | 14 | namespace CodeIgniter\PHPStan\Type; 15 | 16 | use CodeIgniter\Config\BaseService; 17 | use CodeIgniter\Config\Services as FrameworkServices; 18 | use Config\Services as AppServices; 19 | use PHPStan\Analyser\Scope; 20 | use PHPStan\Reflection\ClassReflection; 21 | use PHPStan\Reflection\ParametersAcceptorSelector; 22 | use PHPStan\Reflection\ReflectionProvider; 23 | use PHPStan\ShouldNotHappenException; 24 | use PHPStan\Type\IntersectionType; 25 | use PHPStan\Type\NullType; 26 | use PHPStan\Type\Type; 27 | use PHPStan\Type\TypeTraverser; 28 | use PHPStan\Type\UnionType; 29 | 30 | final class ServicesReturnTypeHelper 31 | { 32 | /** 33 | * @var array 34 | */ 35 | public const IMPOSSIBLE_SERVICE_METHOD_NAMES = [ 36 | '__callstatic', 37 | 'buildservicescache', 38 | 'createrequest', 39 | 'discoverservices', 40 | 'getsharedinstance', 41 | 'injectmock', 42 | 'reset', 43 | 'resetsingle', 44 | 'serviceexists', 45 | ]; 46 | 47 | /** 48 | * @var array 49 | */ 50 | private array $services; 51 | 52 | /** 53 | * @var array 54 | */ 55 | private static array $servicesReflection = []; 56 | 57 | /** 58 | * @param array $additionalServices 59 | */ 60 | public function __construct( 61 | private readonly ReflectionProvider $reflectionProvider, 62 | array $additionalServices, 63 | ) { 64 | $this->services = [ 65 | FrameworkServices::class, 66 | AppServices::class, 67 | ...$additionalServices, 68 | ]; 69 | } 70 | 71 | public function check(Type $type, Scope $scope): Type 72 | { 73 | $this->buildServicesCache(); 74 | 75 | return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($scope): Type { 76 | if ($type instanceof UnionType || $type instanceof IntersectionType) { 77 | return $traverse($type); 78 | } 79 | 80 | foreach ($type->getConstantStrings() as $constantStringType) { 81 | $constantString = $constantStringType->getValue(); 82 | 83 | foreach (self::IMPOSSIBLE_SERVICE_METHOD_NAMES as $impossibleServiceMethodName) { 84 | if (strtolower($constantString) === $impossibleServiceMethodName) { 85 | return new NullType(); 86 | } 87 | } 88 | 89 | $methodReflection = null; 90 | 91 | foreach (self::$servicesReflection as $servicesReflection) { 92 | if ($servicesReflection->hasMethod($constantString)) { 93 | $methodReflection = $servicesReflection->getMethod($constantString, $scope); 94 | } 95 | } 96 | 97 | if ($methodReflection === null) { 98 | return new NullType(); 99 | } 100 | 101 | if (! $methodReflection->isStatic() || ! $methodReflection->isPublic()) { 102 | return new NullType(); 103 | } 104 | 105 | return ParametersAcceptorSelector::selectFromArgs( 106 | $scope, 107 | [], 108 | $methodReflection->getVariants(), 109 | )->getReturnType(); 110 | } 111 | 112 | return new NullType(); 113 | }); 114 | } 115 | 116 | /** 117 | * @return array 118 | */ 119 | public function getServicesReflection(): array 120 | { 121 | $this->buildServicesCache(); 122 | 123 | return self::$servicesReflection; 124 | } 125 | 126 | private function buildServicesCache(): void 127 | { 128 | if (self::$servicesReflection === []) { 129 | self::$servicesReflection = array_map(function (string $service): ClassReflection { 130 | if (! $this->reflectionProvider->hasClass($service)) { 131 | throw new ShouldNotHappenException(sprintf('Services factory class "%s" not found.', $service)); 132 | } 133 | 134 | $serviceReflection = $this->reflectionProvider->getClass($service); 135 | 136 | if ($serviceReflection->getParentClass()?->getName() !== BaseService::class) { 137 | throw new ShouldNotHappenException(sprintf('Services factory class "%s" does not extend %s.', $service, BaseService::class)); 138 | } 139 | 140 | return $serviceReflection; 141 | }, $this->services); 142 | } 143 | } 144 | } 145 | --------------------------------------------------------------------------------