Skip to content

Commit df0d93d

Browse files
committed
Add ServicesFunctionArgumentTypeRule
1 parent ee4afde commit df0d93d

File tree

5 files changed

+193
-0
lines changed

5 files changed

+193
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This extension provides the following features:
1212
* Provides precise return types for `config()` and `model()` functions.
1313
* Provides precise return types for `service()` and `single_service()` functions.
1414
* Checks if the string argument passed to `config()` or `model()` function is a valid class string extending `CodeIgniter\Config\BaseConfig` or `CodeIgniter\Model`, respectively. This can be turned off by setting `codeigniter.checkArgumentTypeOfFactories: false` in your `phpstan.neon`.
15+
* 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`.
1516
* Disallows instantiating cache handlers using `new` and suggests to use the `CacheFactory` class instead.
1617
* Disallows instantiating `FrameworkException` classes using `new`.
1718

extension.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ parameters:
1010
additionalModelNamespaces: []
1111
additionalServices: []
1212
checkArgumentTypeOfFactories: true
13+
checkArgumentTypeOfServices: true
1314

1415
parametersSchema:
1516
codeigniter: structure([
1617
additionalConfigNamespaces: listOf(string())
1718
additionalModelNamespaces: listOf(string())
1819
additionalServices: listOf(string())
1920
checkArgumentTypeOfFactories: bool()
21+
checkArgumentTypeOfServices: bool()
2022
])
2123

2224
services:
@@ -44,9 +46,14 @@ services:
4446
-
4547
class: CodeIgniter\PHPStan\Rules\Functions\FactoriesFunctionArgumentTypeRule
4648

49+
-
50+
class: CodeIgniter\PHPStan\Rules\Functions\ServicesFunctionArgumentTypeRule
51+
4752
conditionalTags:
4853
CodeIgniter\PHPStan\Rules\Functions\FactoriesFunctionArgumentTypeRule:
4954
phpstan.rules.rule: %codeigniter.checkArgumentTypeOfFactories%
55+
CodeIgniter\PHPStan\Rules\Functions\ServicesFunctionArgumentTypeRule:
56+
phpstan.rules.rule: %codeigniter.checkArgumentTypeOfServices%
5057

5158
rules:
5259
- CodeIgniter\PHPStan\Rules\Classes\CacheHandlerInstantiationRule
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
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\ReflectionProvider;
20+
use PHPStan\Rules\Rule;
21+
use PHPStan\Rules\RuleErrorBuilder;
22+
use PHPStan\Type\VerbosityLevel;
23+
24+
/**
25+
* @implements Rule<Node\Expr\FuncCall>
26+
*/
27+
final class ServicesFunctionArgumentTypeRule implements Rule
28+
{
29+
public function __construct(
30+
private readonly ReflectionProvider $reflectionProvider,
31+
private readonly ServicesReturnTypeHelper $servicesReturnTypeHelper
32+
) {}
33+
34+
public function getNodeType(): string
35+
{
36+
return Node\Expr\FuncCall::class;
37+
}
38+
39+
/**
40+
* @param Node\Expr\FuncCall $node
41+
*/
42+
public function processNode(Node $node, Scope $scope): array
43+
{
44+
if (! $node->name instanceof Node\Name) {
45+
return [];
46+
}
47+
48+
$nameNode = $node->name;
49+
$function = $this->reflectionProvider->resolveFunctionName($nameNode, $scope);
50+
51+
if (! in_array($function, ['service', 'single_service'], true)) {
52+
return [];
53+
}
54+
55+
$args = $node->getArgs();
56+
57+
if ($args === []) {
58+
return [];
59+
}
60+
61+
$nameType = $scope->getType($args[0]->value);
62+
63+
if ($nameType->isString()->no()) {
64+
return []; // caught elsewhere
65+
}
66+
67+
$returnType = $this->servicesReturnTypeHelper->check($nameType, $scope);
68+
69+
if ($returnType->isNull()->no()) {
70+
return [];
71+
}
72+
73+
$name = $nameType->describe(VerbosityLevel::precise());
74+
75+
if (in_array(strtolower(trim($name, "'")), ServicesReturnTypeHelper::IMPOSSIBLE_SERVICE_METHOD_NAMES, true)) {
76+
return [RuleErrorBuilder::message(sprintf(
77+
'The name %s is reserved for service location internals and cannot be used as a service name.',
78+
$name
79+
))->identifier('codeigniter.reservedServiceName')->build()];
80+
}
81+
82+
$addTip = static function (RuleErrorBuilder $ruleErrorBuilder) use ($nameType): RuleErrorBuilder {
83+
if ($nameType->getConstantStrings() === []) {
84+
return $ruleErrorBuilder;
85+
}
86+
87+
foreach ($nameType->getConstantStrings() as $constantStringType) {
88+
$ruleErrorBuilder->addTip(sprintf(
89+
'If %s is a valid service name, you can add its possible service class(es) in <fg=cyan>codeigniter.additionalServices</> in your <fg=yellow>%%configurationFile%%</>.',
90+
$constantStringType->describe(VerbosityLevel::precise()),
91+
));
92+
}
93+
94+
return $ruleErrorBuilder;
95+
};
96+
97+
return [$addTip(RuleErrorBuilder::message(sprintf(
98+
'Call to unknown service name %s.',
99+
$nameType->describe(VerbosityLevel::precise())
100+
)))->identifier('codeigniter.unknownServiceName')->build()];
101+
}
102+
}

tests/Fixtures/Type/services.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,7 @@
6363
// this should be overridden by OtherServices
6464
assertType(stdClass::class, service('migrations'));
6565
assertType(Closure::class, service('invoker'));
66+
67+
// gibberish
68+
assertType('null', single_service('bar'));
69+
assertType('null', single_service('timers'));
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
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\Tests\Rules\Functions;
15+
16+
use CodeIgniter\PHPStan\Rules\Functions\ServicesFunctionArgumentTypeRule;
17+
use CodeIgniter\PHPStan\Tests\AdditionalConfigFilesTrait;
18+
use CodeIgniter\PHPStan\Type\ServicesReturnTypeHelper;
19+
use PHPStan\Rules\Rule;
20+
use PHPStan\Testing\RuleTestCase;
21+
use PHPUnit\Framework\Attributes\Group;
22+
23+
/**
24+
* @internal
25+
* @extends RuleTestCase<ServicesFunctionArgumentTypeRule>
26+
*/
27+
#[Group('integration')]
28+
final class ServicesFunctionArgumentTypeRuleTest extends RuleTestCase
29+
{
30+
use AdditionalConfigFilesTrait;
31+
32+
protected function getRule(): Rule
33+
{
34+
return new ServicesFunctionArgumentTypeRule(
35+
self::createReflectionProvider(),
36+
self::getContainer()->getByType(ServicesReturnTypeHelper::class)
37+
);
38+
}
39+
40+
public function testRule(): void
41+
{
42+
$this->analyse([__DIR__ . '/../../Fixtures/Type/services.php'], [
43+
[
44+
'The name \'createRequest\' is reserved for service location internals and cannot be used as a service name.',
45+
51,
46+
],
47+
[
48+
'The name \'__callStatic\' is reserved for service location internals and cannot be used as a service name.',
49+
56,
50+
],
51+
[
52+
'The name \'serviceExists\' is reserved for service location internals and cannot be used as a service name.',
53+
57,
54+
],
55+
[
56+
'The name \'reset\' is reserved for service location internals and cannot be used as a service name.',
57+
58,
58+
],
59+
[
60+
'The name \'resetSingle\' is reserved for service location internals and cannot be used as a service name.',
61+
59,
62+
],
63+
[
64+
'The name \'injectMock\' is reserved for service location internals and cannot be used as a service name.',
65+
60,
66+
],
67+
[
68+
'Call to unknown service name \'bar\'.',
69+
68,
70+
'If \'bar\' is a valid service name, you can add its possible service class(es) in <fg=cyan>codeigniter.additionalServices</> in your <fg=yellow>%configurationFile%</>.',
71+
],
72+
[
73+
'Call to unknown service name \'timers\'.',
74+
69,
75+
'If \'timers\' is a valid service name, you can add its possible service class(es) in <fg=cyan>codeigniter.additionalServices</> in your <fg=yellow>%configurationFile%</>.',
76+
],
77+
]);
78+
}
79+
}

0 commit comments

Comments
 (0)