Skip to content

Commit 9ee914b

Browse files
authored
Cover non-empty-string in substr
1 parent 0492220 commit 9ee914b

File tree

3 files changed

+96
-6
lines changed

3 files changed

+96
-6
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,6 +1275,11 @@ services:
12751275
tags:
12761276
- phpstan.broker.dynamicFunctionReturnTypeExtension
12771277

1278+
-
1279+
class: PHPStan\Type\Php\SubstrDynamicReturnTypeExtension
1280+
tags:
1281+
- phpstan.broker.dynamicFunctionReturnTypeExtension
1282+
12781283
-
12791284
class: PHPStan\Type\Php\ParseUrlFunctionDynamicReturnTypeExtension
12801285
tags:
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Reflection\ParametersAcceptorSelector;
9+
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
10+
use PHPStan\Type\Constant\ConstantIntegerType;
11+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
12+
use PHPStan\Type\IntegerRangeType;
13+
use PHPStan\Type\IntersectionType;
14+
use PHPStan\Type\StringType;
15+
16+
class SubstrDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
17+
{
18+
19+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
20+
{
21+
return $functionReflection->getName() === 'substr';
22+
}
23+
24+
public function getTypeFromFunctionCall(
25+
FunctionReflection $functionReflection,
26+
FuncCall $functionCall,
27+
Scope $scope
28+
): \PHPStan\Type\Type
29+
{
30+
$args = $functionCall->args;
31+
if (count($args) === 0) {
32+
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
33+
}
34+
35+
if (count($args) >= 2) {
36+
$string = $scope->getType($args[0]->value);
37+
$offset = $scope->getType($args[1]->value);
38+
39+
$negativeOffset = IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($offset)->yes();
40+
$zeroOffset = (new ConstantIntegerType(0))->isSuperTypeOf($offset)->yes();
41+
$positiveLength = false;
42+
43+
if (count($args) === 3) {
44+
$length = $scope->getType($args[2]->value);
45+
$positiveLength = IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($length)->yes();
46+
}
47+
48+
if ($string->isNonEmptyString()->yes() && ($negativeOffset || $zeroOffset && $positiveLength)) {
49+
return new IntersectionType([
50+
new StringType(),
51+
new AccessoryNonEmptyStringType(),
52+
]);
53+
}
54+
}
55+
56+
return new StringType();
57+
}
58+
59+
}

tests/PHPStan/Analyser/data/non-empty-string.php

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,29 @@ public function doEmpty2(string $s): void
134134
}
135135
}
136136

137+
/**
138+
* @param non-empty-string $nonEmpty
139+
* @param positive-int $positiveInt
140+
* @param 1|2|3 $postiveRange
141+
* @param -1|-2|-3 $negativeRange
142+
*/
143+
public function doSubstr(string $s, $nonEmpty, $positiveInt, $postiveRange, $negativeRange): void
144+
{
145+
assertType('string', substr($s, 5));
146+
147+
assertType('string', substr($s, -5));
148+
assertType('non-empty-string', substr($nonEmpty, -5));
149+
assertType('non-empty-string', substr($nonEmpty, $negativeRange));
150+
151+
assertType('string', substr($s, 0, 5));
152+
assertType('non-empty-string', substr($nonEmpty, 0, 5));
153+
assertType('non-empty-string', substr($nonEmpty, 0, $postiveRange));
154+
155+
assertType('string', substr($nonEmpty, 0, -5));
156+
157+
assertType('string', substr($s, 0, $positiveInt));
158+
assertType('non-empty-string', substr($nonEmpty, 0, $positiveInt));
159+
}
137160
}
138161

139162
class ImplodingStrings
@@ -186,15 +209,16 @@ public function doFoo4(string $s, array $nonEmptyArrayWithNonEmptyStrings): void
186209
public function sayHello(): void
187210
{
188211
// coming from issue #5291
189-
$s = array(1,2);
212+
$s = array(1, 2);
190213

191214
assertType('non-empty-string', implode("a", $s));
192215
}
193216

194217
/**
195218
* @param non-empty-string $glue
196219
*/
197-
public function nonE($glue, array $a) {
220+
public function nonE($glue, array $a)
221+
{
198222
// coming from issue #5291
199223
if (empty($a)) {
200224
return "xyz";
@@ -206,15 +230,16 @@ public function nonE($glue, array $a) {
206230
public function sayHello2(): void
207231
{
208232
// coming from issue #5291
209-
$s = array(1,2);
233+
$s = array(1, 2);
210234

211235
assertType('non-empty-string', join("a", $s));
212236
}
213237

214238
/**
215239
* @param non-empty-string $glue
216240
*/
217-
public function nonE2($glue, array $a) {
241+
public function nonE2($glue, array $a)
242+
{
218243
// coming from issue #5291
219244
if (empty($a)) {
220245
return "xyz";
@@ -228,7 +253,8 @@ public function nonE2($glue, array $a) {
228253
class LiteralString
229254
{
230255

231-
function x(string $tableName, string $original): void {
256+
function x(string $tableName, string $original): void
257+
{
232258
assertType('non-empty-string', "from `$tableName`");
233259
}
234260

@@ -297,7 +323,7 @@ public function doFoo(string $s, string $nonEmpty, int $i)
297323
assertType('non-empty-string', htmlspecialchars($nonEmpty));
298324
assertType('string', htmlentities($s));
299325
assertType('non-empty-string', htmlentities($nonEmpty));
300-
326+
301327
assertType('string', urlencode($s));
302328
assertType('non-empty-string', urlencode($nonEmpty));
303329
assertType('string', urldecode($s));

0 commit comments

Comments
 (0)