Skip to content

Commit 3b355df

Browse files
committed
Introduce new UseSafeCallablesRule
1 parent 992c773 commit 3b355df

5 files changed

+97
-34
lines changed

phpstan-safe-rule.neon

+4
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:

src/Rules/UseSafeCallablesRule.php

+44
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+
$name = $node->getName();
32+
if (!$name instanceof Node\Name) {
33+
return [];
34+
}
35+
$functionName = $name->toString();
36+
$unsafeFunctions = FunctionListLoader::getFunctionList();
37+
38+
if (isset($unsafeFunctions[$functionName])) {
39+
return [new SafeFunctionRuleError($name, $node->getStartLine())];
40+
}
41+
42+
return [];
43+
}
44+
}

src/Rules/UseSafeFunctionsRule.php

+22-24
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;
@@ -31,41 +30,40 @@ public function getNodeType(): string
3130

3231
public function processNode(Node $node, Scope $scope): array
3332
{
34-
if (!$node->name instanceof Node\Name) {
33+
$name = $node->name;
34+
if (!$name instanceof Node\Name) {
3535
return [];
3636
}
37-
$functionName = $node->name->toString();
37+
$functionName = $name->toString();
3838
$unsafeFunctions = FunctionListLoader::getFunctionList();
3939

4040
if (isset($unsafeFunctions[$functionName])) {
41-
if (! $node->isFirstClassCallable()) {
42-
if ($functionName === "json_decode" || $functionName === "json_encode") {
43-
foreach ($node->args as $arg) {
44-
if ($arg instanceof Node\Arg &&
45-
$arg->name instanceof Node\Identifier &&
46-
$arg->name->toLowerString() === "flags"
47-
) {
48-
if ($this->argValueIncludeJSONTHROWONERROR($arg)) {
49-
return [];
50-
}
41+
if ($functionName === "json_decode" || $functionName === "json_encode") {
42+
foreach ($node->args as $arg) {
43+
if ($arg instanceof Node\Arg &&
44+
$arg->name instanceof Node\Identifier &&
45+
$arg->name->toLowerString() === "flags"
46+
) {
47+
if ($this->argValueIncludeJSONTHROWONERROR($arg)) {
48+
return [];
5149
}
5250
}
5351
}
52+
}
5453

55-
if ($functionName === "json_decode"
56-
&& $this->argValueIncludeJSONTHROWONERROR($node->getArgs()[3] ?? null)
57-
) {
58-
return [];
59-
}
54+
if ($functionName === "json_decode"
55+
&& $this->argValueIncludeJSONTHROWONERROR($node->getArgs()[3] ?? null)
56+
) {
57+
return [];
58+
}
6059

61-
if ($functionName === "json_encode"
62-
&& $this->argValueIncludeJSONTHROWONERROR($node->getArgs()[1] ?? null)
63-
) {
64-
return [];
65-
}
60+
if ($functionName === "json_encode"
61+
&& $this->argValueIncludeJSONTHROWONERROR($node->getArgs()[1] ?? null)
62+
) {
63+
return [];
6664
}
6765

68-
return [new SafeFunctionRuleError($node->name, $node->getStartLine())];
66+
return [new SafeFunctionRuleError($name, $node->getStartLine())];
6967
}
7068

7169
return [];
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 testFirstClassCallable(): void
19+
{
20+
$this->analyse([__DIR__ . '/data/first_class_callable.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+
}

tests/Rules/UseSafeFunctionsRuleTest.php

-10
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,4 @@ public function testJSONEncodeNoCatchSafe(): void
4444
{
4545
$this->analyse([__DIR__ . '/data/safe_json_encode.php'], []);
4646
}
47-
48-
public function testFirstClassCallable(): void
49-
{
50-
$this->analyse([__DIR__ . '/data/first_class_callable.php'], [
51-
[
52-
"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.",
53-
3,
54-
],
55-
]);
56-
}
5747
}

0 commit comments

Comments
 (0)