Skip to content

Commit a14cdce

Browse files
phpstan-botclaude
andcommitted
Use @var PHPDoc type for dynamicConstantNames instead of falling back to mixed
When a class constant has a @var PHPDoc type annotation and is listed in dynamicConstantNames, use the PHPDoc type instead of generalizing the literal value. This fixes the primary use case from phpstan/phpstan#9218 where `@var string|null` on a null-valued constant was ignored. The fix adds a $phpDocType parameter to resolveClassConstantType() and passes it from both call sites in InitializerExprTypeResolver. The priority is: native type > PHPDoc type > generalized value > mixed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 98e77b9 commit a14cdce

4 files changed

Lines changed: 20 additions & 1 deletion

File tree

src/Analyser/ConstantResolver.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ public function resolveConstantType(string $constantName, Type $constantType): T
435435
return $constantType;
436436
}
437437

438-
public function resolveClassConstantType(string $className, string $constantName, Type $constantType, ?Type $nativeType): Type
438+
public function resolveClassConstantType(string $className, string $constantName, Type $constantType, ?Type $nativeType, ?Type $phpDocType): Type
439439
{
440440
$lookupConstantName = sprintf('%s::%s', $className, $constantName);
441441
if (array_key_exists($lookupConstantName, $this->dynamicConstantNames)) {
@@ -458,6 +458,10 @@ public function resolveClassConstantType(string $className, string $constantName
458458
return $nativeType;
459459
}
460460

461+
if ($phpDocType !== null) {
462+
return $phpDocType;
463+
}
464+
461465
if ($constantType->isConstantValue()->yes()) {
462466
$generalized = $constantType->generalize(GeneralizePrecision::lessSpecific());
463467
if ($generalized->isConstantValue()->yes()) {

src/Reflection/InitializerExprTypeResolver.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2544,11 +2544,13 @@ function (Type $type, callable $traverse): Type {
25442544
if ($reflectionConstant->getType() !== null) {
25452545
$nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), selfClass: $constantClassReflection);
25462546
}
2547+
$phpDocType = $constantClassReflection->getConstant($constantName)->getPhpDocType();
25472548
$types[] = $this->constantResolver->resolveClassConstantType(
25482549
$constantClassReflection->getName(),
25492550
$constantName,
25502551
$constantType,
25512552
$nativeType,
2553+
$phpDocType,
25522554
);
25532555
unset($this->currentlyResolvingClassConstant[$resolvingName]);
25542556
continue;
@@ -2577,6 +2579,7 @@ function (Type $type, callable $traverse): Type {
25772579
$constantName,
25782580
$constantType,
25792581
$nativeType,
2582+
$constantReflection->getPhpDocType(),
25802583
);
25812584
unset($this->currentlyResolvingClassConstant[$resolvingName]);
25822585
$types[] = $constantType;

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ class DynamicConstantClass
1818
const DYNAMIC_TRUE_CONSTANT = true;
1919
const DYNAMIC_FALSE_CONSTANT = false;
2020
const DYNAMIC_EMPTY_ARRAY_CONSTANT = [];
21+
22+
/** @var string|null */
23+
const DYNAMIC_NULL_WITH_PHPDOC_CONSTANT = null;
24+
25+
/** @var list<string> */
26+
const DYNAMIC_EMPTY_ARRAY_WITH_PHPDOC_CONSTANT = [];
2127
}
2228

2329
class NoDynamicConstantClass
@@ -45,5 +51,9 @@ private function rip()
4551

4652
// Empty array constant should generalize to mixed
4753
assertType('mixed', DynamicConstantClass::DYNAMIC_EMPTY_ARRAY_CONSTANT);
54+
55+
// Bug 9218: dynamicConstantNames with @var PHPDoc type
56+
assertType('string|null', DynamicConstantClass::DYNAMIC_NULL_WITH_PHPDOC_CONSTANT);
57+
assertType('list<string>', DynamicConstantClass::DYNAMIC_EMPTY_ARRAY_WITH_PHPDOC_CONSTANT);
4858
}
4959
}

tests/PHPStan/Analyser/dynamic-constants.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ parameters:
88
- DynamicConstants\DynamicConstantClass::DYNAMIC_TRUE_CONSTANT
99
- DynamicConstants\DynamicConstantClass::DYNAMIC_FALSE_CONSTANT
1010
- DynamicConstants\DynamicConstantClass::DYNAMIC_EMPTY_ARRAY_CONSTANT
11+
- DynamicConstants\DynamicConstantClass::DYNAMIC_NULL_WITH_PHPDOC_CONSTANT
12+
- DynamicConstants\DynamicConstantClass::DYNAMIC_EMPTY_ARRAY_WITH_PHPDOC_CONSTANT
1113
- GLOBAL_DYNAMIC_CONSTANT
1214
- GLOBAL_DYNAMIC_NULL_CONSTANT
1315
DynamicConstants\DynamicConstantClass::DYNAMIC_CONSTANT_WITH_EXPLICIT_TYPES_IN_CLASS: 'string|null'

0 commit comments

Comments
 (0)