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

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

phpstan.neon

+3-1
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

+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+
$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

+20-10
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

+7-12
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()) {
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(...);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php
2+
3+
var_dump(...);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php
2+
3+
json_encode(...);
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(...);
+42
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+
}

tests/Rules/UseSafeClassesRuleTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ protected function getRule(): Rule
1717

1818
public function testDateTime(): void
1919
{
20-
$this->analyse([__DIR__ . '/data/datetime.php'], [
20+
$this->analyse([__DIR__ . '/UseSafeClassesRule/datetime.php'], [
2121
[
2222
"Class DateTime is unsafe to use. Its methods can return FALSE instead of throwing an exception. Please add 'use Safe\DateTime;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library.",
2323
3,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
3+
$fopen = 'fopen';
4+
$fopen('foobar', 'r');

tests/Rules/data/safe_json_decode.php renamed to tests/Rules/UseSafeFunctionsRule/native_safe.php

+15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
var_dump();
4+
35
// Test various combinations of flags
46
json_decode("{}", true, 512, JSON_THROW_ON_ERROR);
57
json_decode("{}", true, 512, JSON_INVALID_UTF8_IGNORE | JSON_THROW_ON_ERROR);
@@ -12,3 +14,16 @@
1214

1315
// Test named arguments instead of positional
1416
json_decode("{}", flags: JSON_THROW_ON_ERROR);
17+
18+
// Various combinations of flags
19+
json_encode([], JSON_THROW_ON_ERROR, 512);
20+
json_encode([], JSON_FORCE_OBJECT | JSON_THROW_ON_ERROR, 512);
21+
json_encode([], JSON_FORCE_OBJECT | JSON_INVALID_UTF8_IGNORE | JSON_THROW_ON_ERROR, 512);
22+
23+
// Test raw integers too
24+
json_encode([], 4194304, 512);
25+
json_encode([], 16 | 4194304, 512);
26+
json_encode([], 16 | 1048576 | 4194304, 512);
27+
28+
// Named arguments
29+
json_encode([], flags: JSON_THROW_ON_ERROR);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
fopen('foobar', 'r');
4+
json_decode("{}", flags: JSON_INVALID_UTF8_IGNORE);
5+
json_encode([], flags: JSON_FORCE_OBJECT);
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
<?php
2+
3+
use function Safe\fopen;
24
use function Safe\preg_replace;
35

6+
fopen('foobar', 'r');
7+
48
$x = preg_replace('/foo/', 'bar', 'baz');
59
$y = stripos($x, 'foo');
610

7-
$x = preg_replace(['/foo/'], ['bar'], ['baz']);
11+
$x = Safe\preg_replace(['/foo/'], ['bar'], ['baz']);

tests/Rules/UseSafeFunctionsRuleTest.php

+16-13
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,36 @@ protected function getRule(): Rule
1515
return new UseSafeFunctionsRule();
1616
}
1717

18-
public function testCatch(): void
18+
public function testUnsafe(): void
1919
{
20-
$this->analyse([__DIR__ . '/data/fopen.php'], [
20+
$this->analyse([__DIR__ . '/UseSafeFunctionsRule/unsafe.php'], [
2121
[
2222
"Function fopen is unsafe to use. It can return FALSE instead of throwing an exception. Please add 'use function Safe\\fopen;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library.",
23+
3,
24+
],
25+
[
26+
"Function json_decode is unsafe to use. It can return FALSE instead of throwing an exception. Please add 'use function Safe\\json_decode;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library.",
2327
4,
2428
],
29+
[
30+
"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.",
31+
5,
32+
],
2533
]);
2634
}
2735

28-
public function testNoCatchSafe(): void
29-
{
30-
$this->analyse([__DIR__ . '/data/safe_fopen.php'], []);
31-
}
32-
33-
public function testExprCall(): void
36+
public function testUseSafe(): void
3437
{
35-
$this->analyse([__DIR__ . '/data/undirect_call.php'], []);
38+
$this->analyse([__DIR__ . '/UseSafeFunctionsRule/use_safe.php'], []);
3639
}
3740

38-
public function testJSONDecodeNoCatchSafe(): void
41+
public function testNativeSafe(): void
3942
{
40-
$this->analyse([__DIR__ . '/data/safe_json_decode.php'], []);
43+
$this->analyse([__DIR__ . '/UseSafeFunctionsRule/native_safe.php'], []);
4144
}
4245

43-
public function testJSONEncodeNoCatchSafe(): void
46+
public function testExpr(): void
4447
{
45-
$this->analyse([__DIR__ . '/data/safe_json_encode.php'], []);
48+
$this->analyse([__DIR__ . '/UseSafeFunctionsRule/expr.php'], []);
4649
}
4750
}

tests/Rules/data/fopen.php

-5
This file was deleted.

tests/Rules/data/safe_fopen.php

-6
This file was deleted.

tests/Rules/data/safe_json_encode.php

-7
This file was deleted.

tests/Rules/data/undirect_call.php

-5
This file was deleted.

0 commit comments

Comments
 (0)