Skip to content

Commit 4c41073

Browse files
committed
Call RestrictedMethodUsageExtension for first-class callables
1 parent 0850668 commit 4c41073

6 files changed

+322
-0
lines changed

Diff for: conf/config.neon

+2
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ rules:
219219
- PHPStan\Rules\Debug\DumpTypeRule
220220
- PHPStan\Rules\Debug\FileAssertRule
221221
- PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageRule
222+
- PHPStan\Rules\RestrictedUsage\RestrictedMethodCallableUsageRule
222223
- PHPStan\Rules\RestrictedUsage\RestrictedStaticMethodUsageRule
224+
- PHPStan\Rules\RestrictedUsage\RestrictedStaticMethodCallableUsageRule
223225

224226
conditionalTags:
225227
PHPStan\Rules\Exceptions\MissingCheckedExceptionInFunctionThrowsRule:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\RestrictedUsage;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Identifier;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\DependencyInjection\Container;
9+
use PHPStan\Node\MethodCallableNode;
10+
use PHPStan\Reflection\ReflectionProvider;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
14+
/**
15+
* @implements Rule<MethodCallableNode>
16+
*/
17+
final class RestrictedMethodCallableUsageRule implements Rule
18+
{
19+
20+
public function __construct(
21+
private Container $container,
22+
private ReflectionProvider $reflectionProvider,
23+
)
24+
{
25+
}
26+
27+
public function getNodeType(): string
28+
{
29+
return MethodCallableNode::class;
30+
}
31+
32+
/**
33+
* @api
34+
*/
35+
public function processNode(Node $node, Scope $scope): array
36+
{
37+
if (!$node->getName() instanceof Identifier) {
38+
return [];
39+
}
40+
41+
/** @var RestrictedMethodUsageExtension[] $extensions */
42+
$extensions = $this->container->getServicesByTag(RestrictedMethodUsageExtension::METHOD_EXTENSION_TAG);
43+
if ($extensions === []) {
44+
return [];
45+
}
46+
47+
$methodName = $node->getName()->name;
48+
$methodCalledOnType = $scope->getType($node->getVar());
49+
$referencedClasses = $methodCalledOnType->getObjectClassNames();
50+
51+
$errors = [];
52+
53+
foreach ($referencedClasses as $referencedClass) {
54+
if (!$this->reflectionProvider->hasClass($referencedClass)) {
55+
continue;
56+
}
57+
58+
$classReflection = $this->reflectionProvider->getClass($referencedClass);
59+
if (!$classReflection->hasMethod($methodName)) {
60+
continue;
61+
}
62+
63+
$methodReflection = $classReflection->getMethod($methodName, $scope);
64+
foreach ($extensions as $extension) {
65+
$restrictedUsage = $extension->isRestrictedMethodUsage($methodReflection, $scope);
66+
if ($restrictedUsage === null) {
67+
continue;
68+
}
69+
70+
$errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage)
71+
->identifier($restrictedUsage->identifier)
72+
->build();
73+
}
74+
}
75+
76+
return $errors;
77+
}
78+
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\RestrictedUsage;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Identifier;
7+
use PhpParser\Node\Name;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\DependencyInjection\Container;
10+
use PHPStan\Node\StaticMethodCallableNode;
11+
use PHPStan\Reflection\ReflectionProvider;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use PHPStan\Rules\RuleLevelHelper;
15+
use PHPStan\Type\ErrorType;
16+
use PHPStan\Type\Type;
17+
18+
/**
19+
* @implements Rule<StaticMethodCallableNode>
20+
*/
21+
final class RestrictedStaticMethodCallableUsageRule implements Rule
22+
{
23+
24+
public function __construct(
25+
private Container $container,
26+
private ReflectionProvider $reflectionProvider,
27+
private RuleLevelHelper $ruleLevelHelper,
28+
)
29+
{
30+
}
31+
32+
public function getNodeType(): string
33+
{
34+
return StaticMethodCallableNode::class;
35+
}
36+
37+
/**
38+
* @api
39+
*/
40+
public function processNode(Node $node, Scope $scope): array
41+
{
42+
if (!$node->getName() instanceof Identifier) {
43+
return [];
44+
}
45+
46+
/** @var RestrictedMethodUsageExtension[] $extensions */
47+
$extensions = $this->container->getServicesByTag(RestrictedMethodUsageExtension::METHOD_EXTENSION_TAG);
48+
if ($extensions === []) {
49+
return [];
50+
}
51+
52+
$methodName = $node->getName()->name;
53+
$referencedClasses = [];
54+
55+
if ($node->getClass() instanceof Name) {
56+
$referencedClasses[] = $scope->resolveName($node->getClass());
57+
} else {
58+
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
59+
$scope,
60+
$node->getClass(),
61+
'', // We don't care about the error message
62+
static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(),
63+
);
64+
65+
if ($classTypeResult->getType() instanceof ErrorType) {
66+
return [];
67+
}
68+
69+
$referencedClasses = $classTypeResult->getReferencedClasses();
70+
}
71+
72+
$errors = [];
73+
foreach ($referencedClasses as $referencedClass) {
74+
if (!$this->reflectionProvider->hasClass($referencedClass)) {
75+
continue;
76+
}
77+
78+
$classReflection = $this->reflectionProvider->getClass($referencedClass);
79+
if (!$classReflection->hasMethod($methodName)) {
80+
continue;
81+
}
82+
83+
$methodReflection = $classReflection->getMethod($methodName, $scope);
84+
foreach ($extensions as $extension) {
85+
$restrictedUsage = $extension->isRestrictedMethodUsage($methodReflection, $scope);
86+
if ($restrictedUsage === null) {
87+
continue;
88+
}
89+
90+
$errors[] = RuleErrorBuilder::message($restrictedUsage->errorMessage)
91+
->identifier($restrictedUsage->identifier)
92+
->build();
93+
}
94+
}
95+
96+
return $errors;
97+
}
98+
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\RestrictedUsage;
4+
5+
use PHPStan\Rules\Rule as TRule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use const PHP_VERSION_ID;
8+
9+
/**
10+
* @extends RuleTestCase<RestrictedMethodCallableUsageRule>
11+
*/
12+
class RestrictedMethodCallableUsageRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): TRule
16+
{
17+
return new RestrictedMethodCallableUsageRule(
18+
self::getContainer(),
19+
$this->createReflectionProvider(),
20+
);
21+
}
22+
23+
public function testRule(): void
24+
{
25+
if (PHP_VERSION_ID < 80100) {
26+
self::markTestSkipped('Test requires PHP 8.1.');
27+
}
28+
29+
$this->analyse([__DIR__ . '/data/restricted-method-callable.php'], [
30+
[
31+
'Cannot call doFoo',
32+
13,
33+
],
34+
]);
35+
}
36+
37+
public static function getAdditionalConfigFiles(): array
38+
{
39+
return [
40+
__DIR__ . '/restricted-usage.neon',
41+
...parent::getAdditionalConfigFiles(),
42+
];
43+
}
44+
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\RestrictedUsage;
4+
5+
use PHPStan\Rules\Rule as TRule;
6+
use PHPStan\Rules\RuleLevelHelper;
7+
use PHPStan\Testing\RuleTestCase;
8+
use const PHP_VERSION_ID;
9+
10+
/**
11+
* @extends RuleTestCase<RestrictedStaticMethodCallableUsageRule>
12+
*/
13+
class RestrictedStaticMethodCallableUsageRuleTest extends RuleTestCase
14+
{
15+
16+
protected function getRule(): TRule
17+
{
18+
$reflectionProvider = $this->createReflectionProvider();
19+
return new RestrictedStaticMethodCallableUsageRule(
20+
self::getContainer(),
21+
$reflectionProvider,
22+
new RuleLevelHelper($reflectionProvider, true, false, true, true, true, false, true),
23+
);
24+
}
25+
26+
public function testRule(): void
27+
{
28+
if (PHP_VERSION_ID < 80100) {
29+
self::markTestSkipped('Test requires PHP 8.1.');
30+
}
31+
32+
$this->analyse([__DIR__ . '/data/restricted-method-callable.php'], [
33+
[
34+
'Cannot call doFoo',
35+
36,
36+
],
37+
]);
38+
}
39+
40+
public static function getAdditionalConfigFiles(): array
41+
{
42+
return [
43+
__DIR__ . '/restricted-usage.neon',
44+
...parent::getAdditionalConfigFiles(),
45+
];
46+
}
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php // lint >= 8.1
2+
3+
namespace RestrictedMethodCallableUsage;
4+
5+
class Foo
6+
{
7+
8+
public function doTest(Nonexistent $c): void
9+
{
10+
$c->test(...);
11+
$this->doNonexistent(...);
12+
$this->doBar(...);
13+
$this->doFoo(...);
14+
}
15+
16+
public function doBar(): void
17+
{
18+
19+
}
20+
21+
public function doFoo(): void
22+
{
23+
24+
}
25+
26+
}
27+
28+
class FooStatic
29+
{
30+
31+
public static function doTest(): void
32+
{
33+
Nonexistent::test(...);
34+
self::doNonexistent(...);
35+
self::doBar(...);
36+
self::doFoo(...);
37+
}
38+
39+
public static function doBar(): void
40+
{
41+
42+
}
43+
44+
public static function doFoo(): void
45+
{
46+
47+
}
48+
49+
}

0 commit comments

Comments
 (0)