├── 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 | [](https://github.com/CodeIgniter/phpstan-codeigniter/actions/workflows/test-phpunit.yml)
4 | [](https://github.com/CodeIgniter/phpstan-codeigniter/actions/workflows/test-coding-standards.yml)
5 | [](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 |
--------------------------------------------------------------------------------