Skip to content

Commit c937b6d

Browse files
authored
Widen empty ConstantArrayType to array when resolving dynamic constant types (#5606)
1 parent ecf16da commit c937b6d

6 files changed

Lines changed: 99 additions & 2 deletions

File tree

src/Analyser/ConstantResolver.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider;
1414
use PHPStan\ShouldNotHappenException;
1515
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
16+
use PHPStan\Type\ArrayType;
17+
use PHPStan\Type\Constant\ConstantArrayType;
1618
use PHPStan\Type\Constant\ConstantFloatType;
1719
use PHPStan\Type\Constant\ConstantIntegerType;
1820
use PHPStan\Type\Constant\ConstantStringType;
@@ -424,7 +426,7 @@ public function resolveConstantType(string $constantName, Type $constantType): T
424426
return $constantType;
425427
}
426428
if (in_array($constantName, $this->dynamicConstantNames, true)) {
427-
return $constantType->generalize(GeneralizePrecision::lessSpecific());
429+
return $this->generalizeDynamicConstantType($constantType);
428430
}
429431
}
430432

@@ -459,13 +461,23 @@ public function resolveClassConstantType(string $className, string $constantName
459461
}
460462

461463
if ($constantType->isConstantValue()->yes()) {
462-
return $constantType->generalize(GeneralizePrecision::lessSpecific());
464+
return $this->generalizeDynamicConstantType($constantType);
463465
}
464466
}
465467

466468
return $constantType;
467469
}
468470

471+
private function generalizeDynamicConstantType(Type $constantType): Type
472+
{
473+
$generalized = $constantType->generalize(GeneralizePrecision::lessSpecific());
474+
if ($generalized->equals(new ConstantArrayType([], []))) {
475+
return new ArrayType(new MixedType(), new MixedType());
476+
}
477+
478+
return $generalized;
479+
}
480+
469481
private function createInteger(?int $min, ?int $max): Type
470482
{
471483
if ($min !== null && $min === $max) {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PHPStan\Testing\PHPStanTestCase;
6+
use PHPUnit\Framework\Attributes\CoversNothing;
7+
use function array_merge;
8+
use function array_unique;
9+
10+
#[CoversNothing]
11+
class Bug8526IntegrationTest extends PHPStanTestCase
12+
{
13+
14+
public function testBug8526(): void
15+
{
16+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-8526.php');
17+
$this->assertNoErrors($errors);
18+
}
19+
20+
/**
21+
* @return list<Error>
22+
*/
23+
private function runAnalyse(string $file): array
24+
{
25+
$file = $this->getFileHelper()->normalizePath($file);
26+
27+
$analyser = self::getContainer()->getByType(Analyser::class);
28+
$finalizer = self::getContainer()->getByType(AnalyserResultFinalizer::class);
29+
$errors = $finalizer->finalize(
30+
$analyser->analyse([$file], null, null, true),
31+
false,
32+
true,
33+
)->getErrors();
34+
foreach ($errors as $error) {
35+
$this->assertSame($file, $error->getFilePath());
36+
}
37+
38+
return $errors;
39+
}
40+
41+
public static function getAdditionalConfigFiles(): array
42+
{
43+
return array_unique(
44+
array_merge(
45+
parent::getAdditionalConfigFiles(),
46+
[
47+
__DIR__ . '/bug-8526.neon',
48+
],
49+
),
50+
);
51+
}
52+
53+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
includes:
2+
- ../../../conf/bleedingEdge.neon
3+
4+
parameters:
5+
dynamicConstantNames:
6+
- FOO
7+
- DYNAMICARRAY
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Bug8526;
4+
5+
define('FOO', true);
6+
define('DYNAMICARRAY', []);
7+
8+
function doFoo(): void
9+
{
10+
if (isset(DYNAMICARRAY['MyKey'])) {
11+
echo 'has key';
12+
}
13+
if (FOO) {
14+
echo 'foo';
15+
}
16+
}

tests/PHPStan/Analyser/data/dynamic-constant.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
define('GLOBAL_PURE_CONSTANT', 123);
88
define('GLOBAL_DYNAMIC_CONSTANT', false);
99
define('GLOBAL_DYNAMIC_CONSTANT_WITH_EXPLICIT_TYPES', null);
10+
define('GLOBAL_DYNAMIC_EMPTY_ARRAY', []);
11+
define('GLOBAL_NON_DYNAMIC_EMPTY_ARRAY', []); // not listed in `dynamicConstantNames` NEON config
1012

1113
class DynamicConstantClass
1214
{
@@ -22,6 +24,8 @@ class DynamicConstantClass
2224

2325
/** @var int */
2426
const DYNAMIC_INCOMPATIBLE_PHPDOC_CONSTANT = null;
27+
28+
const DYNAMIC_EMPTY_ARRAY_NO_PHPDOC = [];
2529
}
2630

2731
class NoDynamicConstantClass
@@ -41,5 +45,8 @@ private function rip()
4145
assertType('string|null', DynamicConstantClass::DYNAMIC_NULL_WITH_PHPDOC_CONSTANT);
4246
assertType('list<string>', DynamicConstantClass::DYNAMIC_EMPTY_ARRAY_WITH_PHPDOC_CONSTANT);
4347
assertType('int', DynamicConstantClass::DYNAMIC_INCOMPATIBLE_PHPDOC_CONSTANT);
48+
assertType('array', DynamicConstantClass::DYNAMIC_EMPTY_ARRAY_NO_PHPDOC);
49+
assertType('array', GLOBAL_DYNAMIC_EMPTY_ARRAY);
50+
assertType('array{}', GLOBAL_NON_DYNAMIC_EMPTY_ARRAY);
4451
}
4552
}

tests/PHPStan/Analyser/dynamic-constants.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ parameters:
77
- DynamicConstants\DynamicConstantClass::DYNAMIC_NULL_WITH_PHPDOC_CONSTANT
88
- DynamicConstants\DynamicConstantClass::DYNAMIC_EMPTY_ARRAY_WITH_PHPDOC_CONSTANT
99
- DynamicConstants\DynamicConstantClass::DYNAMIC_INCOMPATIBLE_PHPDOC_CONSTANT
10+
- DynamicConstants\DynamicConstantClass::DYNAMIC_EMPTY_ARRAY_NO_PHPDOC
1011
- GLOBAL_DYNAMIC_CONSTANT
12+
- GLOBAL_DYNAMIC_EMPTY_ARRAY
1113
DynamicConstants\DynamicConstantClass::DYNAMIC_CONSTANT_WITH_EXPLICIT_TYPES_IN_CLASS: 'string|null'
1214
GLOBAL_DYNAMIC_CONSTANT_WITH_EXPLICIT_TYPES: 'string|null'

0 commit comments

Comments
 (0)