Skip to content

Commit 9616f3f

Browse files
ondrejmirtesclaude
andcommitted
Add get_class() throw type extension
PhpStorm stubs now declare @throws \TypeError and \Error on get_class(), which made every call an explicit throw point. It throws TypeError only for an argument that is not an object, and Error only when called without an argument outside of a class; PHP 7 only warns. The object check reads the native type, so a PHPDoc-only object type keeps the TypeError. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dhb4ssXuKVWcVBcNFpsdn7
1 parent 9260927 commit 9616f3f

3 files changed

Lines changed: 195 additions & 0 deletions

File tree

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 Error;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\DependencyInjection\AutowiredService;
9+
use PHPStan\Php\PhpVersion;
10+
use PHPStan\Reflection\FunctionReflection;
11+
use PHPStan\Type\DynamicFunctionThrowTypeExtension;
12+
use PHPStan\Type\ObjectType;
13+
use PHPStan\Type\Type;
14+
use PHPStan\Type\VoidType;
15+
use TypeError;
16+
use function count;
17+
18+
/**
19+
* get_class() throws TypeError for an argument that is not an object, and
20+
* Error when called without an argument outside of a class.
21+
*/
22+
#[AutowiredService]
23+
final class GetClassFunctionThrowTypeExtension 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() === 'get_class';
33+
}
34+
35+
public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type
36+
{
37+
if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) {
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) === 0) {
49+
return $scope->isInClass() ? new VoidType() : new ObjectType(Error::class);
50+
}
51+
52+
if ($scope->getNativeType($args[0]->value)->isObject()->yes()) {
53+
return new VoidType();
54+
}
55+
56+
return new ObjectType(TypeError::class);
57+
}
58+
59+
}

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,6 +1230,72 @@ public function testPrintfThrowTypeBeforePhp8(): void
12301230
]);
12311231
}
12321232

1233+
#[RequiresPhp('>= 8.0.0')]
1234+
public function testGetClassThrowType(): void
1235+
{
1236+
$this->analyse([__DIR__ . '/data/get-class-throw-type.php'], [
1237+
[
1238+
'Dead catch - TypeError is never thrown in the try block.',
1239+
16,
1240+
],
1241+
[
1242+
'Dead catch - TypeError is never thrown in the try block.',
1243+
22,
1244+
],
1245+
[
1246+
'Dead catch - Error is never thrown in the try block.',
1247+
28,
1248+
],
1249+
[
1250+
'Dead catch - Error is never thrown in the try block.',
1251+
35,
1252+
],
1253+
[
1254+
'Dead catch - TypeError is never thrown in the try block.',
1255+
55,
1256+
],
1257+
]);
1258+
}
1259+
1260+
#[RequiresPhp('< 8.0.0')]
1261+
public function testGetClassThrowTypeBeforePhp8(): void
1262+
{
1263+
$this->analyse([__DIR__ . '/data/get-class-throw-type.php'], [
1264+
[
1265+
'Dead catch - TypeError is never thrown in the try block.',
1266+
16,
1267+
],
1268+
[
1269+
'Dead catch - TypeError is never thrown in the try block.',
1270+
22,
1271+
],
1272+
[
1273+
'Dead catch - Error is never thrown in the try block.',
1274+
28,
1275+
],
1276+
[
1277+
'Dead catch - Error is never thrown in the try block.',
1278+
35,
1279+
],
1280+
[
1281+
'Dead catch - TypeError is never thrown in the try block.',
1282+
42,
1283+
],
1284+
[
1285+
'Dead catch - TypeError is never thrown in the try block.',
1286+
48,
1287+
],
1288+
[
1289+
'Dead catch - TypeError is never thrown in the try block.',
1290+
55,
1291+
],
1292+
[
1293+
'Dead catch - Error is never thrown in the try block.',
1294+
67,
1295+
],
1296+
]);
1297+
}
1298+
12331299
#[RequiresPhp('>= 8.4.0')]
12341300
public function testRoundThrowType(): void
12351301
{
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
namespace GetClassThrowType;
4+
5+
class Foo
6+
{
7+
8+
/**
9+
* @param \stdClass $phpDocObject
10+
* @param mixed $mixed
11+
*/
12+
public function doFoo(\stdClass $o, $phpDocObject, $mixed): void
13+
{
14+
try {
15+
$a = get_class($o);
16+
} catch (\TypeError $e) {
17+
18+
}
19+
20+
try {
21+
$a = get_class($this);
22+
} catch (\TypeError $e) {
23+
24+
}
25+
26+
try {
27+
$a = get_class();
28+
} catch (\Error $e) {
29+
30+
}
31+
32+
$f = function (): string {
33+
try {
34+
return get_class();
35+
} catch (\Error $e) {
36+
return '';
37+
}
38+
};
39+
40+
try {
41+
$a = get_class($phpDocObject);
42+
} catch (\TypeError $e) {
43+
44+
}
45+
46+
try {
47+
$a = get_class($mixed);
48+
} catch (\TypeError $e) {
49+
50+
}
51+
52+
if (is_object($mixed)) {
53+
try {
54+
$a = get_class($mixed);
55+
} catch (\TypeError $e) {
56+
57+
}
58+
}
59+
}
60+
61+
}
62+
63+
function doBar(): void
64+
{
65+
try {
66+
$a = get_class();
67+
} catch (\Error $e) {
68+
69+
}
70+
}

0 commit comments

Comments
 (0)