Skip to content

Commit 084c97e

Browse files
ondrejmirtesclaude
andcommitted
Add str_split() throw type extension
PhpStorm stubs now declare @throws \ValueError on str_split(), which made every call an explicit throw point. It is thrown only when $length is less than 1, and PHP 7 only warns. The decision reads the native type of $length, so a PHPDoc-only positive-int keeps the ValueError. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dhb4ssXuKVWcVBcNFpsdn7
1 parent 7ef0c4c commit 084c97e

3 files changed

Lines changed: 158 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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\DependencyInjection\AutowiredService;
8+
use PHPStan\Php\PhpVersion;
9+
use PHPStan\Reflection\FunctionReflection;
10+
use PHPStan\Type\DynamicFunctionThrowTypeExtension;
11+
use PHPStan\Type\IntegerRangeType;
12+
use PHPStan\Type\Type;
13+
use PHPStan\Type\VoidType;
14+
use function count;
15+
16+
/**
17+
* str_split() throws ValueError only when $length is less than 1.
18+
*/
19+
#[AutowiredService]
20+
final class StrSplitFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension
21+
{
22+
23+
public function __construct(private PhpVersion $phpVersion)
24+
{
25+
}
26+
27+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
28+
{
29+
return $functionReflection->getName() === 'str_split';
30+
}
31+
32+
public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type
33+
{
34+
if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) {
35+
return new VoidType();
36+
}
37+
38+
$args = $funcCall->getArgs();
39+
foreach ($args as $arg) {
40+
if ($arg->unpack || $arg->name !== null) {
41+
return $functionReflection->getThrowType();
42+
}
43+
}
44+
45+
if (count($args) < 2) {
46+
return new VoidType();
47+
}
48+
49+
if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($scope->getNativeType($args[1]->value))->yes()) {
50+
return new VoidType();
51+
}
52+
53+
return $functionReflection->getThrowType();
54+
}
55+
56+
}

‎tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,56 @@ public function testRoundThrowTypeBeforePhp84(): void
12461246
]);
12471247
}
12481248

1249+
#[RequiresPhp('>= 8.0.0')]
1250+
public function testStrSplitThrowType(): void
1251+
{
1252+
$this->analyse([__DIR__ . '/data/str-split-throw-type.php'], [
1253+
[
1254+
'Dead catch - ValueError is never thrown in the try block.',
1255+
15,
1256+
],
1257+
[
1258+
'Dead catch - ValueError is never thrown in the try block.',
1259+
21,
1260+
],
1261+
[
1262+
'Dead catch - ValueError is never thrown in the try block.',
1263+
46,
1264+
],
1265+
]);
1266+
}
1267+
1268+
#[RequiresPhp('< 8.0.0')]
1269+
public function testStrSplitThrowTypeBeforePhp8(): void
1270+
{
1271+
$this->analyse([__DIR__ . '/data/str-split-throw-type.php'], [
1272+
[
1273+
'Dead catch - ValueError is never thrown in the try block.',
1274+
15,
1275+
],
1276+
[
1277+
'Dead catch - ValueError is never thrown in the try block.',
1278+
21,
1279+
],
1280+
[
1281+
'Dead catch - ValueError is never thrown in the try block.',
1282+
27,
1283+
],
1284+
[
1285+
'Dead catch - ValueError is never thrown in the try block.',
1286+
33,
1287+
],
1288+
[
1289+
'Dead catch - ValueError is never thrown in the try block.',
1290+
39,
1291+
],
1292+
[
1293+
'Dead catch - ValueError is never thrown in the try block.',
1294+
46,
1295+
],
1296+
]);
1297+
}
1298+
12491299
public function testDsMapVoidThrowType(): void
12501300
{
12511301
$this->analyse([__DIR__ . '/data/ds-map-void-throw-type.php'], [
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace StrSplitThrowType;
4+
5+
class Foo
6+
{
7+
8+
/**
9+
* @param positive-int $phpDocLength
10+
*/
11+
public function doFoo(string $s, int $length, int $phpDocLength): void
12+
{
13+
try {
14+
$a = str_split($s);
15+
} catch (\ValueError $e) {
16+
17+
}
18+
19+
try {
20+
$a = str_split($s, 3);
21+
} catch (\ValueError $e) {
22+
23+
}
24+
25+
try {
26+
$a = str_split($s, 0);
27+
} catch (\ValueError $e) {
28+
29+
}
30+
31+
try {
32+
$a = str_split($s, $length);
33+
} catch (\ValueError $e) {
34+
35+
}
36+
37+
try {
38+
$a = str_split($s, $phpDocLength);
39+
} catch (\ValueError $e) {
40+
41+
}
42+
43+
if ($length > 0) {
44+
try {
45+
$a = str_split($s, $length);
46+
} catch (\ValueError $e) {
47+
48+
}
49+
}
50+
}
51+
52+
}

0 commit comments

Comments
 (0)