Skip to content

Commit 7ef0c4c

Browse files
ondrejmirtesclaude
andcommitted
Add round() throw type extension
PhpStorm stubs now declare @throws \ValueError on round(), which made every call an explicit throw point. It is thrown only on PHP 8.4+, for an int $mode that is not one of the eight rounding modes (1-8); older versions accept any mode. The decision reads the native type of $mode, so a PHPDoc-only valid constant keeps the ValueError. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dhb4ssXuKVWcVBcNFpsdn7
1 parent 239052a commit 7ef0c4c

4 files changed

Lines changed: 196 additions & 0 deletions

File tree

‎src/Php/PhpVersion.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,11 @@ public function hasStricterRoundFunctions(): bool
246246
return $this->versionId >= 80000;
247247
}
248248

249+
public function throwsValueErrorForInvalidRoundingMode(): bool
250+
{
251+
return $this->versionId >= 80400;
252+
}
253+
249254
public function hasTentativeReturnTypes(): bool
250255
{
251256
return $this->versionId >= 80100;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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\ObjectType;
13+
use PHPStan\Type\Type;
14+
use PHPStan\Type\TypeCombinator;
15+
use PHPStan\Type\VoidType;
16+
use function count;
17+
18+
/**
19+
* round() throws ValueError only on PHP 8.4+, for an int $mode that is not
20+
* one of the eight rounding modes (1-8).
21+
*/
22+
#[AutowiredService]
23+
final class RoundFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension
24+
{
25+
26+
public function __construct(private PhpVersion $phpVersion)
27+
{
28+
}
29+
30+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
31+
{
32+
return $functionReflection->getName() === 'round';
33+
}
34+
35+
public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type
36+
{
37+
if (!$this->phpVersion->throwsValueErrorForInvalidRoundingMode()) {
38+
return new VoidType();
39+
}
40+
41+
$args = $funcCall->getArgs();
42+
foreach ($args as $arg) {
43+
if ($arg->unpack || $arg->name !== null) {
44+
return $functionReflection->getThrowType();
45+
}
46+
}
47+
48+
if (count($args) < 3) {
49+
return new VoidType();
50+
}
51+
52+
$validModeType = TypeCombinator::union(
53+
new ObjectType('RoundingMode'),
54+
IntegerRangeType::fromInterval(1, 8),
55+
);
56+
if ($validModeType->isSuperTypeOf($scope->getNativeType($args[2]->value))->yes()) {
57+
return new VoidType();
58+
}
59+
60+
return $functionReflection->getThrowType();
61+
}
62+
63+
}

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,6 +1180,72 @@ public function testPrintfThrowTypeBeforePhp8(): void
11801180
]);
11811181
}
11821182

1183+
#[RequiresPhp('>= 8.4.0')]
1184+
public function testRoundThrowType(): void
1185+
{
1186+
$this->analyse([__DIR__ . '/data/round-throw-type.php'], [
1187+
[
1188+
'Dead catch - ValueError is never thrown in the try block.',
1189+
15,
1190+
],
1191+
[
1192+
'Dead catch - ValueError is never thrown in the try block.',
1193+
21,
1194+
],
1195+
[
1196+
'Dead catch - ValueError is never thrown in the try block.',
1197+
27,
1198+
],
1199+
[
1200+
'Dead catch - ValueError is never thrown in the try block.',
1201+
33,
1202+
],
1203+
[
1204+
'Dead catch - ValueError is never thrown in the try block.',
1205+
39,
1206+
],
1207+
]);
1208+
}
1209+
1210+
#[RequiresPhp('< 8.4.0')]
1211+
public function testRoundThrowTypeBeforePhp84(): void
1212+
{
1213+
$this->analyse([__DIR__ . '/data/round-throw-type.php'], [
1214+
[
1215+
'Dead catch - ValueError is never thrown in the try block.',
1216+
15,
1217+
],
1218+
[
1219+
'Dead catch - ValueError is never thrown in the try block.',
1220+
21,
1221+
],
1222+
[
1223+
'Dead catch - ValueError is never thrown in the try block.',
1224+
27,
1225+
],
1226+
[
1227+
'Dead catch - ValueError is never thrown in the try block.',
1228+
33,
1229+
],
1230+
[
1231+
'Dead catch - ValueError is never thrown in the try block.',
1232+
39,
1233+
],
1234+
[
1235+
'Dead catch - ValueError is never thrown in the try block.',
1236+
45,
1237+
],
1238+
[
1239+
'Dead catch - ValueError is never thrown in the try block.',
1240+
51,
1241+
],
1242+
[
1243+
'Dead catch - ValueError is never thrown in the try block.',
1244+
57,
1245+
],
1246+
]);
1247+
}
1248+
11831249
public function testDsMapVoidThrowType(): void
11841250
{
11851251
$this->analyse([__DIR__ . '/data/ds-map-void-throw-type.php'], [
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace RoundThrowType;
4+
5+
class Foo
6+
{
7+
8+
/**
9+
* @param 1 $phpDocMode
10+
*/
11+
public function doFoo(float $f, int $mode, int $phpDocMode): void
12+
{
13+
try {
14+
$a = round($f);
15+
} catch (\ValueError $e) {
16+
17+
}
18+
19+
try {
20+
$a = round($f, 2);
21+
} catch (\ValueError $e) {
22+
23+
}
24+
25+
try {
26+
$a = round($f, 0, PHP_ROUND_HALF_EVEN);
27+
} catch (\ValueError $e) {
28+
29+
}
30+
31+
try {
32+
$a = round($f, 0, 8);
33+
} catch (\ValueError $e) {
34+
35+
}
36+
37+
try {
38+
$a = round($f, 0, \RoundingMode::HalfEven);
39+
} catch (\ValueError $e) {
40+
41+
}
42+
43+
try {
44+
$a = round($f, 0, 9);
45+
} catch (\ValueError $e) {
46+
47+
}
48+
49+
try {
50+
$a = round($f, 0, $mode);
51+
} catch (\ValueError $e) {
52+
53+
}
54+
55+
try {
56+
$a = round($f, 0, $phpDocMode);
57+
} catch (\ValueError $e) {
58+
59+
}
60+
}
61+
62+
}

0 commit comments

Comments
 (0)