Skip to content

Commit 00f4845

Browse files
authored
Merge pull request #10 from thecodingmachine/preg_replace
Adding Safe\preg_replace support
2 parents ffbdebd + 47cbb9e commit 00f4845

File tree

5 files changed

+133
-0
lines changed

5 files changed

+133
-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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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+
class ReplaceSafeFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
19+
{
20+
21+
/** @var array<string, int> */
22+
private $functions = [
23+
'Safe\preg_replace' => 2,
24+
];
25+
26+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
27+
{
28+
return array_key_exists($functionReflection->getName(), $this->functions);
29+
}
30+
31+
public function getTypeFromFunctionCall(
32+
FunctionReflection $functionReflection,
33+
FuncCall $functionCall,
34+
Scope $scope
35+
): Type {
36+
$type = $this->getPreliminarilyResolvedTypeFromFunctionCall($functionReflection, $functionCall, $scope);
37+
38+
$possibleTypes = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
39+
40+
if (TypeCombinator::containsNull($possibleTypes)) {
41+
$type = TypeCombinator::addNull($type);
42+
}
43+
44+
return $type;
45+
}
46+
47+
private function getPreliminarilyResolvedTypeFromFunctionCall(
48+
FunctionReflection $functionReflection,
49+
FuncCall $functionCall,
50+
Scope $scope
51+
): Type {
52+
$argumentPosition = $this->functions[$functionReflection->getName()];
53+
if (count($functionCall->args) <= $argumentPosition) {
54+
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
55+
}
56+
57+
$subjectArgumentType = $scope->getType($functionCall->args[$argumentPosition]->value);
58+
$defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
59+
if ($subjectArgumentType instanceof MixedType) {
60+
return TypeUtils::toBenevolentUnion($defaultReturnType);
61+
}
62+
$stringType = new StringType();
63+
$arrayType = new ArrayType(new MixedType(), new MixedType());
64+
65+
$isStringSuperType = $stringType->isSuperTypeOf($subjectArgumentType);
66+
$isArraySuperType = $arrayType->isSuperTypeOf($subjectArgumentType);
67+
$compareSuperTypes = $isStringSuperType->compareTo($isArraySuperType);
68+
if ($compareSuperTypes === $isStringSuperType) {
69+
return $stringType;
70+
} elseif ($compareSuperTypes === $isArraySuperType) {
71+
if ($subjectArgumentType instanceof ArrayType) {
72+
return $subjectArgumentType->generalizeValues();
73+
}
74+
return $subjectArgumentType;
75+
}
76+
77+
return $defaultReturnType;
78+
}
79+
}

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)