Skip to content

Commit d38d4e4

Browse files
committed
Detect first class callable syntax calls
This is the syntax supported by PHP 8.1: ``` $callable = print_r(...); $callable(42); ``` or ``` $blade = new \Waldo\Quux\Blade(); $callable = $blade->runner(...); $callable(303); ``` The errors for the disallowed code are reported on lines with `(...)`, not when the callable is called. Ref #275
1 parent 9e5b799 commit d38d4e4

23 files changed

+586
-132
lines changed

extension.neon

+1
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ services:
254254
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedAttributeRuleErrors
255255
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedConstantRuleErrors
256256
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedControlStructureRuleErrors
257+
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors
257258
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedMethodRuleErrors
258259
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedNamespaceRuleErrors
259260
- Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors

src/Allowed/Allowed.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ private function getArgType(array $args, Scope $scope, Param $param): ?Type
181181
if (!isset($found)) {
182182
$found = $args[$param->getPosition() - 1] ?? null;
183183
}
184-
return isset($found) ? $scope->getType($found->value) : null;
184+
return isset($found, $found->value) ? $scope->getType($found->value) : null;
185185
}
186186

187187

src/Calls/FunctionCalls.php

+6-61
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,13 @@
55

66
use PhpParser\Node;
77
use PhpParser\Node\Expr\FuncCall;
8-
use PhpParser\Node\Expr\Variable;
9-
use PhpParser\Node\Name;
10-
use PhpParser\Node\Scalar\String_;
118
use PHPStan\Analyser\Scope;
12-
use PHPStan\Reflection\ReflectionProvider;
139
use PHPStan\Rules\IdentifierRuleError;
1410
use PHPStan\Rules\Rule;
1511
use PHPStan\ShouldNotHappenException;
1612
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
1713
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
18-
use Spaze\PHPStan\Rules\Disallowed\Normalizer\Normalizer;
19-
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedCallsRuleErrors;
20-
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\ErrorIdentifiers;
21-
use Spaze\PHPStan\Rules\Disallowed\Type\TypeResolver;
14+
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors;
2215

2316
/**
2417
* Reports on dynamically calling a disallowed function.
@@ -29,42 +22,27 @@
2922
class FunctionCalls implements Rule
3023
{
3124

32-
private DisallowedCallsRuleErrors $disallowedCallsRuleErrors;
25+
private DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors;
3326

3427
/** @var list<DisallowedCall> */
3528
private array $disallowedCalls;
3629

37-
private ReflectionProvider $reflectionProvider;
38-
39-
private Normalizer $normalizer;
40-
41-
private TypeResolver $typeResolver;
42-
4330

4431
/**
45-
* @param DisallowedCallsRuleErrors $disallowedCallsRuleErrors
32+
* @param DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors
4633
* @param DisallowedCallFactory $disallowedCallFactory
47-
* @param ReflectionProvider $reflectionProvider
48-
* @param Normalizer $normalizer
49-
* @param TypeResolver $typeResolver
5034
* @param array $forbiddenCalls
5135
* @phpstan-param ForbiddenCallsConfig $forbiddenCalls
5236
* @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config
5337
* @throws ShouldNotHappenException
5438
*/
5539
public function __construct(
56-
DisallowedCallsRuleErrors $disallowedCallsRuleErrors,
40+
DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors,
5741
DisallowedCallFactory $disallowedCallFactory,
58-
ReflectionProvider $reflectionProvider,
59-
Normalizer $normalizer,
60-
TypeResolver $typeResolver,
6142
array $forbiddenCalls
6243
) {
63-
$this->disallowedCallsRuleErrors = $disallowedCallsRuleErrors;
44+
$this->disallowedFunctionRuleErrors = $disallowedFunctionRuleErrors;
6445
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
65-
$this->reflectionProvider = $reflectionProvider;
66-
$this->normalizer = $normalizer;
67-
$this->typeResolver = $typeResolver;
6846
}
6947

7048

@@ -82,40 +60,7 @@ public function getNodeType(): string
8260
*/
8361
public function processNode(Node $node, Scope $scope): array
8462
{
85-
if ($node->name instanceof Name) {
86-
$namespacedName = $node->name->getAttribute('namespacedName');
87-
if ($namespacedName !== null && !($namespacedName instanceof Name)) {
88-
throw new ShouldNotHappenException();
89-
}
90-
$names = [$namespacedName, $node->name];
91-
} elseif ($node->name instanceof String_) {
92-
$names = [new Name($this->normalizer->normalizeNamespace($node->name->value))];
93-
} elseif ($node->name instanceof Variable) {
94-
$value = $this->typeResolver->getVariableStringValue($node->name, $scope);
95-
if (!is_string($value)) {
96-
return [];
97-
}
98-
$names = [new Name($this->normalizer->normalizeNamespace($value))];
99-
} else {
100-
return [];
101-
}
102-
$displayName = $node->name->getAttribute('originalName');
103-
if ($displayName !== null && !($displayName instanceof Name)) {
104-
throw new ShouldNotHappenException();
105-
}
106-
foreach ($names as $name) {
107-
if ($name && $this->reflectionProvider->hasFunction($name, $scope)) {
108-
$functionReflection = $this->reflectionProvider->getFunction($name, $scope);
109-
$definedIn = $functionReflection->isBuiltin() ? null : $functionReflection->getFileName();
110-
} else {
111-
$definedIn = null;
112-
}
113-
$message = $this->disallowedCallsRuleErrors->get($node, $scope, (string)$name, (string)($displayName ?? $name), $definedIn, $this->disallowedCalls, ErrorIdentifiers::DISALLOWED_FUNCTION);
114-
if ($message) {
115-
return $message;
116-
}
117-
}
118-
return [];
63+
return $this->disallowedFunctionRuleErrors->get($node, $scope, $this->disallowedCalls);
11964
}
12065

12166
}
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
namespace Spaze\PHPStan\Rules\Disallowed\Calls;
5+
6+
use PhpParser\Node;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Node\FunctionCallableNode;
9+
use PHPStan\Rules\IdentifierRuleError;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\ShouldNotHappenException;
12+
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
13+
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
14+
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedFunctionRuleErrors;
15+
16+
/**
17+
* Reports on first class callable syntax for a disallowed method.
18+
*
19+
* @package Spaze\PHPStan\Rules\Disallowed
20+
* @implements Rule<FunctionCallableNode>
21+
*/
22+
class FunctionFirstClassCallables implements Rule
23+
{
24+
25+
private DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors;
26+
27+
/** @var list<DisallowedCall> */
28+
private array $disallowedCalls;
29+
30+
31+
/**
32+
* @param DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors
33+
* @param DisallowedCallFactory $disallowedCallFactory
34+
* @param array $forbiddenCalls
35+
* @phpstan-param ForbiddenCallsConfig $forbiddenCalls
36+
* @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config
37+
* @throws ShouldNotHappenException
38+
*/
39+
public function __construct(
40+
DisallowedFunctionRuleErrors $disallowedFunctionRuleErrors,
41+
DisallowedCallFactory $disallowedCallFactory,
42+
array $forbiddenCalls
43+
) {
44+
$this->disallowedFunctionRuleErrors = $disallowedFunctionRuleErrors;
45+
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
46+
}
47+
48+
49+
public function getNodeType(): string
50+
{
51+
return FunctionCallableNode::class;
52+
}
53+
54+
55+
/**
56+
* @param FunctionCallableNode $node
57+
* @param Scope $scope
58+
* @return list<IdentifierRuleError>
59+
* @throws ShouldNotHappenException
60+
*/
61+
public function processNode(Node $node, Scope $scope): array
62+
{
63+
$originalNode = $node->getOriginalNode();
64+
return $this->disallowedFunctionRuleErrors->get($originalNode, $scope, $this->disallowedCalls);
65+
}
66+
67+
}
+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
namespace Spaze\PHPStan\Rules\Disallowed\Calls;
5+
6+
use PhpParser\Node;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Node\MethodCallableNode;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleError;
11+
use PHPStan\ShouldNotHappenException;
12+
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
13+
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
14+
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedMethodRuleErrors;
15+
16+
/**
17+
* Reports on first class callable syntax for a disallowed method.
18+
*
19+
* Static callables have a different rule, <code>StaticFirstClassCallables</code>
20+
*
21+
* @package Spaze\PHPStan\Rules\Disallowed
22+
* @implements Rule<MethodCallableNode>
23+
*/
24+
class MethodFirstClassCallables implements Rule
25+
{
26+
27+
private DisallowedMethodRuleErrors $disallowedMethodRuleErrors;
28+
29+
/** @var list<DisallowedCall> */
30+
private array $disallowedCalls;
31+
32+
33+
/**
34+
* @param DisallowedMethodRuleErrors $disallowedMethodRuleErrors
35+
* @param DisallowedCallFactory $disallowedCallFactory
36+
* @param array $forbiddenCalls
37+
* @phpstan-param ForbiddenCallsConfig $forbiddenCalls
38+
* @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config
39+
* @throws ShouldNotHappenException
40+
*/
41+
public function __construct(DisallowedMethodRuleErrors $disallowedMethodRuleErrors, DisallowedCallFactory $disallowedCallFactory, array $forbiddenCalls)
42+
{
43+
$this->disallowedMethodRuleErrors = $disallowedMethodRuleErrors;
44+
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
45+
}
46+
47+
48+
public function getNodeType(): string
49+
{
50+
return MethodCallableNode::class;
51+
}
52+
53+
54+
/**
55+
* @param MethodCallableNode $node
56+
* @param Scope $scope
57+
* @return list<RuleError>
58+
* @throws ShouldNotHappenException
59+
*/
60+
public function processNode(Node $node, Scope $scope): array
61+
{
62+
$originalNode = $node->getOriginalNode();
63+
return $this->disallowedMethodRuleErrors->get($originalNode->var, $originalNode, $scope, $this->disallowedCalls);
64+
}
65+
66+
}
+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
namespace Spaze\PHPStan\Rules\Disallowed\Calls;
5+
6+
use PhpParser\Node;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Node\StaticMethodCallableNode;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleError;
11+
use PHPStan\ShouldNotHappenException;
12+
use Spaze\PHPStan\Rules\Disallowed\DisallowedCall;
13+
use Spaze\PHPStan\Rules\Disallowed\DisallowedCallFactory;
14+
use Spaze\PHPStan\Rules\Disallowed\RuleErrors\DisallowedMethodRuleErrors;
15+
16+
/**
17+
* Reports on first class callable syntax for a disallowed static method.
18+
*
19+
* Dynamic calls have a different rule, <code>MethodFirstClassCallables</code>
20+
*
21+
* @package Spaze\PHPStan\Rules\Disallowed
22+
* @implements Rule<StaticMethodCallableNode>
23+
*/
24+
class StaticFirstClassCallables implements Rule
25+
{
26+
27+
private DisallowedMethodRuleErrors $disallowedMethodRuleErrors;
28+
29+
/** @var list<DisallowedCall> */
30+
private array $disallowedCalls;
31+
32+
33+
/**
34+
* @param DisallowedMethodRuleErrors $disallowedMethodRuleErrors
35+
* @param DisallowedCallFactory $disallowedCallFactory
36+
* @param array $forbiddenCalls
37+
* @phpstan-param ForbiddenCallsConfig $forbiddenCalls
38+
* @noinspection PhpUndefinedClassInspection ForbiddenCallsConfig is a type alias defined in PHPStan config
39+
* @throws ShouldNotHappenException
40+
*/
41+
public function __construct(DisallowedMethodRuleErrors $disallowedMethodRuleErrors, DisallowedCallFactory $disallowedCallFactory, array $forbiddenCalls)
42+
{
43+
$this->disallowedMethodRuleErrors = $disallowedMethodRuleErrors;
44+
$this->disallowedCalls = $disallowedCallFactory->createFromConfig($forbiddenCalls);
45+
}
46+
47+
48+
public function getNodeType(): string
49+
{
50+
return StaticMethodCallableNode::class;
51+
}
52+
53+
54+
/**
55+
* @param StaticMethodCallableNode $node
56+
* @param Scope $scope
57+
* @return list<RuleError>
58+
* @throws ShouldNotHappenException
59+
*/
60+
public function processNode(Node $node, Scope $scope): array
61+
{
62+
$originalNode = $node->getOriginalNode();
63+
return $this->disallowedMethodRuleErrors->get($originalNode->class, $originalNode, $scope, $this->disallowedCalls);
64+
}
65+
66+
}

0 commit comments

Comments
 (0)