Skip to content

Commit 5f9795e

Browse files
authored
Merge pull request #32 from mll-lab/handle-potential-first-class-callable
Handle FuncCall nodes that represent first class callables
2 parents 518dfde + 9d145a4 commit 5f9795e

21 files changed

+181
-61
lines changed

phpstan-safe-rule.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
services:
2+
-
3+
class: TheCodingMachine\Safe\PHPStan\Rules\UseSafeCallablesRule
4+
tags:
5+
- phpstan.rules.rule
26
-
37
class: TheCodingMachine\Safe\PHPStan\Rules\UseSafeFunctionsRule
48
tags:

phpstan.neon

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ parameters:
44
- src
55
- tests
66
excludePaths:
7-
- tests/Rules/data
7+
- tests/Rules/UseSafeCallablesRule
8+
- tests/Rules/UseSafeClassesRule
9+
- tests/Rules/UseSafeFunctionsRule
810
ignoreErrors:
911
-
1012
message: '#^Implementing PHPStan\\Rules\\IdentifierRuleError is not covered by backward compatibility promise\. The interface might change in a minor PHPStan version\.$#'

src/Rules/UseSafeCallablesRule.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace TheCodingMachine\Safe\PHPStan\Rules;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Node\FunctionCallableNode;
9+
use TheCodingMachine\Safe\PHPStan\Rules\Error\SafeFunctionRuleError;
10+
use TheCodingMachine\Safe\PHPStan\Utils\FunctionListLoader;
11+
12+
/**
13+
* This rule checks that no "unsafe" functions are used in code.
14+
*
15+
* @implements Rule<FunctionCallableNode>
16+
*/
17+
class UseSafeCallablesRule implements Rule
18+
{
19+
/**
20+
* @see JSON_THROW_ON_ERROR
21+
*/
22+
const JSON_THROW_ON_ERROR = 4194304;
23+
24+
public function getNodeType(): string
25+
{
26+
return FunctionCallableNode::class;
27+
}
28+
29+
public function processNode(Node $node, Scope $scope): array
30+
{
31+
$nodeName = $node->getName();
32+
if (!$nodeName instanceof Node\Name) {
33+
return [];
34+
}
35+
$functionName = $nodeName->toString();
36+
$unsafeFunctions = FunctionListLoader::getFunctionList();
37+
38+
if (isset($unsafeFunctions[$functionName])) {
39+
return [new SafeFunctionRuleError($nodeName, $node->getStartLine())];
40+
}
41+
42+
return [];
43+
}
44+
}

src/Rules/UseSafeFunctionsRule.php

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<?php
22

3-
43
namespace TheCodingMachine\Safe\PHPStan\Rules;
54

65
use PhpParser\Node;
@@ -19,17 +18,23 @@
1918
*/
2019
class UseSafeFunctionsRule implements Rule
2120
{
21+
/**
22+
* @see JSON_THROW_ON_ERROR
23+
*/
24+
const JSON_THROW_ON_ERROR = 4194304;
25+
2226
public function getNodeType(): string
2327
{
2428
return Node\Expr\FuncCall::class;
2529
}
2630

2731
public function processNode(Node $node, Scope $scope): array
2832
{
29-
if (!$node->name instanceof Node\Name) {
33+
$nodeName = $node->name;
34+
if (!$nodeName instanceof Node\Name) {
3035
return [];
3136
}
32-
$functionName = $node->name->toString();
37+
$functionName = $nodeName->toString();
3338
$unsafeFunctions = FunctionListLoader::getFunctionList();
3439

3540
if (isset($unsafeFunctions[$functionName])) {
@@ -58,7 +63,7 @@ public function processNode(Node $node, Scope $scope): array
5863
return [];
5964
}
6065

61-
return [new SafeFunctionRuleError($node->name, $node->getStartLine())];
66+
return [new SafeFunctionRuleError($nodeName, $node->getStartLine())];
6267
}
6368

6469
return [];
@@ -87,11 +92,16 @@ private function argValueIncludeJSONTHROWONERROR(?Arg $arg): bool
8792
return true;
8893
}
8994

90-
return in_array(true, array_map(function ($element) {
91-
// JSON_THROW_ON_ERROR == 4194304
92-
return ($element & 4194304) == 4194304;
93-
}, array_filter($options, function ($element) {
94-
return is_int($element);
95-
})), true);
95+
$intOptions = array_filter($options, function (mixed $option): bool {
96+
return is_int($option);
97+
});
98+
99+
foreach ($intOptions as $option) {
100+
if (($option & self::JSON_THROW_ON_ERROR) === self::JSON_THROW_ON_ERROR) {
101+
return true;
102+
}
103+
}
104+
105+
return false;
96106
}
97107
}

src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,17 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
5656
Scope $scope
5757
): Type {
5858
$argumentPosition = $this->functions[$functionReflection->getName()];
59-
$defaultReturnType = ParametersAcceptorSelector::selectFromArgs(
60-
$scope,
61-
$functionCall->getArgs(),
62-
$functionReflection->getVariants()
63-
)
59+
60+
$args = $functionCall->getArgs();
61+
$variants = $functionReflection->getVariants();
62+
$defaultReturnType = ParametersAcceptorSelector::selectFromArgs($scope, $args, $variants)
6463
->getReturnType();
65-
66-
if (count($functionCall->args) <= $argumentPosition) {
67-
return $defaultReturnType;
68-
}
6964

70-
$subjectArgument = $functionCall->args[$argumentPosition];
71-
if (!$subjectArgument instanceof Arg) {
65+
if (count($args) <= $argumentPosition) {
7266
return $defaultReturnType;
7367
}
74-
68+
69+
$subjectArgument = $args[$argumentPosition];
7570
$subjectArgumentType = $scope->getType($subjectArgument->value);
7671
$mixedType = new MixedType();
7772
if ($subjectArgumentType->isSuperTypeOf($mixedType)->yes()) {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
3+
$json_encode_name = 'json_encode';
4+
$encode_from_expression = $json_encode_name(...);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php
2+
3+
var_dump(...);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php
2+
3+
json_encode(...);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
use function Safe\json_encode;
4+
5+
json_encode(...);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace TheCodingMachine\Safe\PHPStan\Rules;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @template-extends RuleTestCase<UseSafeCallablesRule>
10+
*/
11+
class UseSafeCallablesRuleTest extends RuleTestCase
12+
{
13+
protected function getRule(): Rule
14+
{
15+
return new UseSafeCallablesRule();
16+
}
17+
18+
public function testUnsafe(): void
19+
{
20+
$this->analyse([__DIR__ . '/UseSafeCallablesRule/unsafe.php'], [
21+
[
22+
"Function json_encode is unsafe to use. It can return FALSE instead of throwing an exception. Please add 'use function Safe\\json_encode;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library.",
23+
3,
24+
],
25+
]);
26+
}
27+
28+
public function testUseSafe(): void
29+
{
30+
$this->analyse([__DIR__ . '/UseSafeCallablesRule/use_safe.php'], []);
31+
}
32+
33+
public function testNativeSafe(): void
34+
{
35+
$this->analyse([__DIR__ . '/UseSafeCallablesRule/native_safe.php'], []);
36+
}
37+
38+
public function testExpr(): void
39+
{
40+
$this->analyse([__DIR__ . '/UseSafeCallablesRule/expr.php'], []);
41+
}
42+
}

0 commit comments

Comments
 (0)