Skip to content

Commit 44fe04f

Browse files
committed
Detect disallowed functions and methods in callable parameters
Reuses existing detection from DisallowedFunctionRuleErrors (for simple `'function'` callables) and DisallowedMethodRuleErrors (for `[$object, 'method']` callables) called from the new DisallowedCallableParameterRuleErrors service. Ref #275
1 parent 9fdeff6 commit 44fe04f

25 files changed

+1182
-59
lines changed

extension.neon

+1
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ services:
252252
- Spaze\PHPStan\Rules\Disallowed\Identifier\Identifier
253253
- Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer
254254
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedAttributeRuleErrors
255+
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallableParameterRuleErrors(forbiddenFunctionCalls: %disallowedFunctionCalls%, forbiddenMethodCalls: %disallowedMethodCalls%, forbiddenStaticCalls: %disallowedStaticCalls%)
255256
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedConstantRuleErrors
256257
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedControlStructureRuleErrors
257258
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors

src/Calls/FunctionCalls.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\ShouldNotHappenException;
1212
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
1313
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
14+
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallableParameterRuleErrors;
1415
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors;
1516

1617
/**
@@ -24,12 +25,15 @@ class FunctionCalls implements Rule
2425

2526
private DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors;
2627

28+
private DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors;
29+
2730
/** @var list<DisallowedCall> */
2831
private array $disallowedCalls;
2932

3033

3134
/**
3235
* @param DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors
36+
* @param DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors
3337
* @param DisallowedCallFactory $disallowedCallFactory
3438
* @param array $forbiddenCalls
3539
* @phpstan-param ForbiddenCallsConfig $forbiddenCalls
@@ -38,10 +42,12 @@ class FunctionCalls implements Rule
3842
*/
3943
public function __construct(
4044
DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors,
45+
DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors,
4146
DisallowedCallFactory $disallowedCallFactory,
4247
array $forbiddenCalls
4348
) {
4449
$this->disallowedFunctionRuleErrors = $disallowedFunctionRuleErrors;
50+
$this->disallowedCallableParameterRuleErrors = $disallowedCallableParameterRuleErrors;
4551
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
4652
}
4753

@@ -60,7 +66,9 @@ public function getNodeType(): string
6066
*/
6167
public function processNode(Node $node, Scope $scope): array
6268
{
63-
return $this->disallowedFunctionRuleErrors->get($node, $scope, $this->disallowedCalls);
69+
$errors = $this->disallowedFunctionRuleErrors->get($node, $scope, $this->disallowedCalls);
70+
$paramErrors = $this->disallowedCallableParameterRuleErrors->getForFunction($node, $scope);
71+
return $errors || $paramErrors ? array_merge($errors, $paramErrors) : [];
6472
}
6573

6674
}

src/Calls/MethodCalls.php

+14-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\ShouldNotHappenException;
1212
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
1313
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
14+
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallableParameterRuleErrors;
1415
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedMethodRuleErrors;
1516

1617
/**
@@ -26,22 +27,30 @@ class MethodCalls implements Rule
2627

2728
private DisallowedMethodRuleErrors $disallowedMethodRuleErrors;
2829

30+
private DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors;
31+
2932
/** @var list<DisallowedCall> */
3033
private array $disallowedCalls;
3134

3235

3336
/**
3437
* @param DisallowedMethodRuleErrors $disallowedMethodRuleErrors
38+
* @param DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors
3539
* @param DisallowedCallFactory $disallowedCallFactory
3640
* @param array $forbiddenCalls
3741
* @phpstan-param ForbiddenCallsConfig $forbiddenCalls
3842
* @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config
3943
* @throws ShouldNotHappenException
4044
*/
41-
public function __construct(DisallowedMethodRuleErrors $disallowedMethodRuleErrors, DisallowedCallFactory $disallowedCallFactory, array $forbiddenCalls)
42-
{
45+
public function __construct(
46+
DisallowedMethodRuleErrors $disallowedMethodRuleErrors,
47+
DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors,
48+
DisallowedCallFactory $disallowedCallFactory,
49+
array $forbiddenCalls
50+
) {
4351
$this->disallowedMethodRuleErrors = $disallowedMethodRuleErrors;
4452
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
53+
$this->disallowedCallableParameterRuleErrors = $disallowedCallableParameterRuleErrors;
4554
}
4655

4756

@@ -59,7 +68,9 @@ public function getNodeType(): string
5968
*/
6069
public function processNode(Node $node, Scope $scope): array
6170
{
62-
return $this->disallowedMethodRuleErrors->get($node->var, $node, $scope, $this->disallowedCalls);
71+
$errors = $this->disallowedMethodRuleErrors->get($node->var, $node, $scope, $this->disallowedCalls);
72+
$paramErrors = $this->disallowedCallableParameterRuleErrors->getForMethod($node->var, $node, $scope);
73+
return $errors || $paramErrors ? array_merge($errors, $paramErrors) : [];
6374
}
6475

6576
}

src/Calls/StaticCalls.php

+14-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\ShouldNotHappenException;
1212
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
1313
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
14+
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallableParameterRuleErrors;
1415
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedMethodRuleErrors;
1516

1617
/**
@@ -26,22 +27,30 @@ class StaticCalls implements Rule
2627

2728
private DisallowedMethodRuleErrors $disallowedMethodRuleErrors;
2829

30+
private DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors;
31+
2932
/** @var list<DisallowedCall> */
3033
private array $disallowedCalls;
3134

3235

3336
/**
3437
* @param DisallowedMethodRuleErrors $disallowedMethodRuleErrors
38+
* @param DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors
3539
* @param DisallowedCallFactory $disallowedCallFactory
3640
* @param array $forbiddenCalls
3741
* @phpstan-param ForbiddenCallsConfig $forbiddenCalls
3842
* @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config
3943
* @throws ShouldNotHappenException
4044
*/
41-
public function __construct(DisallowedMethodRuleErrors $disallowedMethodRuleErrors, DisallowedCallFactory $disallowedCallFactory, array $forbiddenCalls)
42-
{
45+
public function __construct(
46+
DisallowedMethodRuleErrors $disallowedMethodRuleErrors,
47+
DisallowedCallableParameterRuleErrors $disallowedCallableParameterRuleErrors,
48+
DisallowedCallFactory $disallowedCallFactory,
49+
array $forbiddenCalls
50+
) {
4351
$this->disallowedMethodRuleErrors = $disallowedMethodRuleErrors;
4452
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
53+
$this->disallowedCallableParameterRuleErrors = $disallowedCallableParameterRuleErrors;
4554
}
4655

4756

@@ -59,7 +68,9 @@ public function getNodeType(): string
5968
*/
6069
public function processNode(Node $node, Scope $scope): array
6170
{
62-
return $this->disallowedMethodRuleErrors->get($node->class, $node, $scope, $this->disallowedCalls);
71+
$errors = $this->disallowedMethodRuleErrors->get($node->class, $node, $scope, $this->disallowedCalls);
72+
$paramErrors = $this->disallowedCallableParameterRuleErrors->getForMethod($node->class, $node, $scope);
73+
return $errors || $paramErrors ? array_merge($errors, $paramErrors) : [];
6374
}
6475

6576
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
namespace Spaze\PHPStan\Rules\Disallowed\RuleErrors;
5+
6+
use PhpParser\Node\Expr;
7+
use PhpParser\Node\Expr\CallLike;
8+
use PhpParser\Node\Expr\FuncCall;
9+
use PhpParser\Node\Expr\MethodCall;
10+
use PhpParser\Node\Expr\StaticCall;
11+
use PhpParser\Node\Name;
12+
use PHPStan\Analyser\ArgumentsNormalizer;
13+
use PHPStan\Analyser\Scope;
14+
use PHPStan\Reflection\ExtendedMethodReflection;
15+
use PHPStan\Reflection\FunctionReflection;
16+
use PHPStan\Reflection\ParametersAcceptorSelector;
17+
use PHPStan\Reflection\ReflectionProvider;
18+
use PHPStan\Rules\IdentifierRuleError;
19+
use PHPStan\ShouldNotHappenException;
20+
use PHPStan\Type\TypeCombinator;
21+
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
22+
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
23+
use Spaze\PHPStan\Rules\Disallowed\PHPStan1Compatibility;
24+
use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver;
25+
26+
class DisallowedCallableParameterRuleErrors
27+
{
28+
29+
private TypeResolver $typeResolver;
30+
31+
private DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors;
32+
33+
private DisallowedMethodRuleErrors $disallowedMethodRuleErrors;
34+
35+
/** @var list<DisallowedCall> */
36+
private array $disallowedFunctionCalls;
37+
38+
/** @var list<DisallowedCall> */
39+
private array $disallowedCalls;
40+
41+
private ReflectionProvider $reflectionProvider;
42+
43+
44+
/**
45+
* @param TypeResolver $typeResolver
46+
* @param DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors
47+
* @param DisallowedMethodRuleErrors $disallowedMethodRuleErrors
48+
* @param DisallowedCallFactory $disallowedCallFactory
49+
* @param ReflectionProvider $reflectionProvider
50+
* @param array $forbiddenFunctionCalls
51+
* @phpstan-param ForbiddenCallsConfig $forbiddenFunctionCalls
52+
* @param array $forbiddenMethodCalls
53+
* @phpstan-param ForbiddenCallsConfig $forbiddenMethodCalls
54+
* @param array $forbiddenStaticCalls
55+
* @phpstan-param ForbiddenCallsConfig $forbiddenStaticCalls
56+
* @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config
57+
* @throws ShouldNotHappenException
58+
*/
59+
public function __construct(
60+
TypeResolver $typeResolver,
61+
DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors,
62+
DisallowedMethodRuleErrors $disallowedMethodRuleErrors,
63+
DisallowedCallFactory $disallowedCallFactory,
64+
ReflectionProvider $reflectionProvider,
65+
array $forbiddenFunctionCalls,
66+
array $forbiddenMethodCalls,
67+
array $forbiddenStaticCalls
68+
) {
69+
$this->typeResolver = $typeResolver;
70+
$this->disallowedFunctionRuleErrors = $disallowedFunctionRuleErrors;
71+
$this->disallowedMethodRuleErrors = $disallowedMethodRuleErrors;
72+
$this->disallowedFunctionCalls = $disallowedCallFactory->createFromConfig($forbiddenFunctionCalls);
73+
$this->disallowedCalls = $disallowedCallFactory->createFromConfig(array_merge($forbiddenMethodCalls, $forbiddenStaticCalls));
74+
$this->reflectionProvider = $reflectionProvider;
75+
}
76+
77+
78+
/**
79+
* @param FuncCall $node
80+
* @param Scope $scope
81+
* @return list<IdentifierRuleError>
82+
* @throws ShouldNotHappenException
83+
*/
84+
public function getForFunction(FuncCall $node, Scope $scope): array
85+
{
86+
$ruleErrors = [];
87+
foreach ($this->typeResolver->getNamesFromCall($node, $scope) as $name) {
88+
if (!$this->reflectionProvider->hasFunction($name, $scope)) {
89+
continue;
90+
}
91+
$reflection = $this->reflectionProvider->getFunction($name, $scope);
92+
$errors = $this->getErrors($node, $scope, $reflection);
93+
if ($errors) {
94+
$ruleErrors = array_merge($ruleErrors, $errors);
95+
}
96+
}
97+
return $ruleErrors;
98+
}
99+
100+
101+
/**
102+
* @param Name|Expr $class
103+
* @param MethodCall|StaticCall $node
104+
* @param Scope $scope
105+
* @return list<IdentifierRuleError>
106+
* @throws ShouldNotHappenException
107+
*/
108+
public function getForMethod($class, CallLike $node, Scope $scope): array
109+
{
110+
$ruleErrors = [];
111+
$classType = $this->typeResolver->getType($class, $scope);
112+
if (PHPStan1Compatibility::isClassString($classType)->yes()) {
113+
$classType = $classType->getClassStringObjectType();
114+
}
115+
foreach ($classType->getObjectTypeOrClassStringObjectType()->getObjectClassNames() as $className) {
116+
if (!$this->reflectionProvider->hasClass($className)) {
117+
continue;
118+
}
119+
$classReflection = $this->reflectionProvider->getClass($className);
120+
foreach ($this->typeResolver->getNamesFromCall($node, $scope) as $name) {
121+
if (!$classReflection->hasMethod($name->toString())) {
122+
continue;
123+
}
124+
$reflection = $classReflection->getMethod($name->toString(), $scope);
125+
$errors = $this->getErrors($node, $scope, $reflection);
126+
if ($errors) {
127+
$ruleErrors = array_merge($ruleErrors, $errors);
128+
}
129+
}
130+
}
131+
return $ruleErrors;
132+
}
133+
134+
135+
/**
136+
* @param Scope $scope
137+
* @param FuncCall|MethodCall|StaticCall $node
138+
* @param ExtendedMethodReflection|FunctionReflection $reflection
139+
* @return list<IdentifierRuleError>
140+
* @throws ShouldNotHappenException
141+
*/
142+
private function getErrors(CallLike $node, Scope $scope, $reflection): array
143+
{
144+
$ruleErrors = [];
145+
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $reflection->getVariants());
146+
$reorderedArgs = ArgumentsNormalizer::reorderArgs($parametersAcceptor, $node->getArgs()) ?? $node->getArgs();
147+
foreach ($parametersAcceptor->getParameters() as $key => $parameter) {
148+
if (!TypeCombinator::removeNull($parameter->getType())->isCallable()->yes() || !isset($reorderedArgs[$key])) {
149+
continue;
150+
}
151+
$callableType = $scope->getType($reorderedArgs[$key]->value);
152+
foreach ($callableType->getConstantStrings() as $constantString) {
153+
$errors = $this->disallowedFunctionRuleErrors->getByString($constantString->getValue(), $scope, $this->disallowedFunctionCalls);
154+
if ($errors) {
155+
$ruleErrors = array_merge($ruleErrors, $errors);
156+
}
157+
}
158+
foreach ($callableType->getConstantArrays() as $constantArray) {
159+
foreach ($constantArray->findTypeAndMethodNames() as $typeAndMethodName) {
160+
if ($typeAndMethodName->isUnknown()) {
161+
continue;
162+
}
163+
$method = $typeAndMethodName->getMethod();
164+
foreach ($typeAndMethodName->getType()->getObjectClassNames() as $class) {
165+
$errors = $this->disallowedMethodRuleErrors->getByString($class, $method, $scope, $this->disallowedCalls);
166+
if ($errors) {
167+
$ruleErrors = array_merge($ruleErrors, $errors);
168+
}
169+
}
170+
}
171+
}
172+
}
173+
return $ruleErrors;
174+
}
175+
176+
}

0 commit comments

Comments
 (0)