Skip to content

Commit b1df559

Browse files
committed
Adding Safe\preg_replace support
1 parent bc9696f commit b1df559

File tree

5 files changed

+137
-0
lines changed

5 files changed

+137
-0
lines changed

phpstan-safe-rule.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ services:
33
class: TheCodingMachine\Safe\PHPStan\Rules\UseSafeFunctionsRule
44
tags:
55
- phpstan.rules.rule
6+
-
7+
class: TheCodingMachine\Safe\PHPStan\Type\Php\ReplaceSafeFunctionsDynamicReturnTypeExtension
8+
tags:
9+
- phpstan.broker.dynamicFunctionReturnTypeExtension
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php declare(strict_types = 1);
2+
3+
4+
namespace TheCodingMachine\Safe\PHPStan\Type\Php;
5+
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\FunctionReflection;
9+
use PHPStan\Reflection\ParametersAcceptorSelector;
10+
use PHPStan\Type\ArrayType;
11+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
12+
use PHPStan\Type\MixedType;
13+
use PHPStan\Type\StringType;
14+
use PHPStan\Type\Type;
15+
use PHPStan\Type\TypeCombinator;
16+
use PHPStan\Type\TypeUtils;
17+
18+
19+
class ReplaceSafeFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
20+
{
21+
22+
/** @var array<string, int> */
23+
private $functions = [
24+
'Safe\preg_replace' => 2,
25+
];
26+
27+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
28+
{
29+
return array_key_exists($functionReflection->getName(), $this->functions);
30+
}
31+
32+
public function getTypeFromFunctionCall(
33+
FunctionReflection $functionReflection,
34+
FuncCall $functionCall,
35+
Scope $scope
36+
): Type
37+
{
38+
$type = $this->getPreliminarilyResolvedTypeFromFunctionCall($functionReflection, $functionCall, $scope);
39+
40+
$possibleTypes = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
41+
42+
if (TypeCombinator::containsNull($possibleTypes)) {
43+
$type = TypeCombinator::addNull($type);
44+
}
45+
46+
return $type;
47+
}
48+
49+
private function getPreliminarilyResolvedTypeFromFunctionCall(
50+
FunctionReflection $functionReflection,
51+
FuncCall $functionCall,
52+
Scope $scope
53+
): Type
54+
{
55+
$argumentPosition = $this->functions[$functionReflection->getName()];
56+
if (count($functionCall->args) <= $argumentPosition) {
57+
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
58+
}
59+
60+
$subjectArgumentType = $scope->getType($functionCall->args[$argumentPosition]->value);
61+
$defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
62+
if ($subjectArgumentType instanceof MixedType) {
63+
return TypeUtils::toBenevolentUnion($defaultReturnType);
64+
}
65+
$stringType = new StringType();
66+
$arrayType = new ArrayType(new MixedType(), new MixedType());
67+
68+
$isStringSuperType = $stringType->isSuperTypeOf($subjectArgumentType);
69+
$isArraySuperType = $arrayType->isSuperTypeOf($subjectArgumentType);
70+
$compareSuperTypes = $isStringSuperType->compareTo($isArraySuperType);
71+
if ($compareSuperTypes === $isStringSuperType) {
72+
return $stringType;
73+
} elseif ($compareSuperTypes === $isArraySuperType) {
74+
if ($subjectArgumentType instanceof ArrayType) {
75+
return $subjectArgumentType->generalizeValues();
76+
}
77+
return $subjectArgumentType;
78+
}
79+
80+
return $defaultReturnType;
81+
}
82+
83+
}

tests/Rules/CallMethodRuleTest.php

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\FunctionCallParametersCheck;
6+
use PHPStan\Rules\Methods\CallMethodsRule;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Rules\RuleLevelHelper;
9+
use PHPStan\Testing\RuleTestCase;
10+
use TheCodingMachine\Safe\PHPStan\Type\Php\ReplaceSafeFunctionsDynamicReturnTypeExtension;
11+
12+
class CallMethodRuleTest extends RuleTestCase
13+
{
14+
protected function getRule(): Rule
15+
{
16+
$broker = $this->createBroker();
17+
$ruleLevelHelper = new RuleLevelHelper($broker, true, true, true);
18+
return new CallMethodsRule(
19+
$broker,
20+
new FunctionCallParametersCheck($ruleLevelHelper, true, true),
21+
$ruleLevelHelper,
22+
true,
23+
true
24+
);
25+
}
26+
27+
public function testSafePregReplace()
28+
{
29+
// FIXME: this rule actually runs code but will always return no error because the rule executed is not the correct one.
30+
// This provides code coverage but assert is not ok.
31+
$this->analyse([__DIR__ . '/data/safe_pregreplace.php'], []);
32+
}
33+
34+
35+
/**
36+
* @return \PHPStan\Type\DynamicFunctionReturnTypeExtension[]
37+
*/
38+
public function getDynamicFunctionReturnTypeExtensions(): array
39+
{
40+
return [new ReplaceSafeFunctionsDynamicReturnTypeExtension()];
41+
}
42+
}

tests/Rules/UseSafeFunctionsRuleTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace TheCodingMachine\Safe\PHPStan\Rules;
44

55
use PHPStan\Testing\RuleTestCase;
6+
use TheCodingMachine\Safe\PHPStan\Type\Php\ReplaceSafeFunctionsDynamicReturnTypeExtension;
67

78
class UseSafeFunctionsRuleTest extends RuleTestCase
89
{

tests/Rules/data/safe_pregreplace.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
use function Safe\preg_replace;
3+
4+
$x = preg_replace('/foo/', 'bar', 'baz');
5+
$y = stripos($x, 'foo');
6+
7+
$x = preg_replace(['/foo/'], ['bar'], ['baz']);

0 commit comments

Comments
 (0)