Skip to content

Commit be378e1

Browse files
herndlmondrejmirtes
authored andcommitted
Add return type extension for constant()
1 parent a540e44 commit be378e1

File tree

6 files changed

+136
-20
lines changed

6 files changed

+136
-20
lines changed

conf/config.neon

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,6 +1363,14 @@ services:
13631363
arguments:
13641364
checkMaybeUndefinedVariables: %checkMaybeUndefinedVariables%
13651365

1366+
-
1367+
class: PHPStan\Type\Php\ConstantFunctionReturnTypeExtension
1368+
tags:
1369+
- phpstan.broker.dynamicFunctionReturnTypeExtension
1370+
1371+
-
1372+
class: PHPStan\Type\Php\ConstantHelper
1373+
13661374
-
13671375
class: PHPStan\Type\Php\CountFunctionReturnTypeExtension
13681376
tags:
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\Reflection\FunctionReflection;
8+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
9+
use PHPStan\Type\Type;
10+
use PHPStan\Type\TypeCombinator;
11+
use function count;
12+
13+
class ConstantFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
14+
{
15+
16+
public function __construct(private ConstantHelper $constantHelper)
17+
{
18+
}
19+
20+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
21+
{
22+
return $functionReflection->getName() === 'constant';
23+
}
24+
25+
public function getTypeFromFunctionCall(
26+
FunctionReflection $functionReflection,
27+
FuncCall $functionCall,
28+
Scope $scope,
29+
): ?Type
30+
{
31+
if (count($functionCall->getArgs()) < 1) {
32+
return null;
33+
}
34+
35+
$nameType = $scope->getType($functionCall->getArgs()[0]->value);
36+
37+
$results = [];
38+
foreach ($nameType->getConstantStrings() as $constantName) {
39+
$results[] = $scope->getType($this->constantHelper->createExprFromConstantName($constantName->getValue()));
40+
}
41+
42+
if (count($results) > 0) {
43+
return TypeCombinator::union(...$results);
44+
}
45+
46+
return null;
47+
}
48+
49+
}

src/Type/Php/ConstantHelper.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr;
6+
use PhpParser\Node\Expr\ClassConstFetch;
7+
use PhpParser\Node\Expr\ConstFetch;
8+
use PhpParser\Node\Identifier;
9+
use PhpParser\Node\Name;
10+
use PhpParser\Node\Name\FullyQualified;
11+
use function count;
12+
use function explode;
13+
use function ltrim;
14+
15+
class ConstantHelper
16+
{
17+
18+
public function createExprFromConstantName(string $constantName): Expr
19+
{
20+
$classConstParts = explode('::', $constantName);
21+
if (count($classConstParts) >= 2) {
22+
$classConstName = new FullyQualified(ltrim($classConstParts[0], '\\'));
23+
if ($classConstName->isSpecialClassName()) {
24+
$classConstName = new Name($classConstName->toString());
25+
}
26+
27+
return new ClassConstFetch($classConstName, new Identifier($classConstParts[1]));
28+
}
29+
30+
return new ConstFetch(new FullyQualified($constantName));
31+
}
32+
33+
}

src/Type/Php/DefinedConstantTypeSpecifyingExtension.php

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace PHPStan\Type\Php;
44

5-
use PhpParser\Node;
65
use PhpParser\Node\Expr\FuncCall;
76
use PHPStan\Analyser\Scope;
87
use PHPStan\Analyser\SpecifiedTypes;
@@ -14,14 +13,16 @@
1413
use PHPStan\Type\FunctionTypeSpecifyingExtension;
1514
use PHPStan\Type\MixedType;
1615
use function count;
17-
use function explode;
18-
use function ltrim;
1916

2017
class DefinedConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
2118
{
2219

2320
private TypeSpecifier $typeSpecifier;
2421

22+
public function __construct(private ConstantHelper $constantHelper)
23+
{
24+
}
25+
2526
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
2627
{
2728
$this->typeSpecifier = $typeSpecifier;
@@ -53,24 +54,8 @@ public function specifyTypes(
5354
return new SpecifiedTypes([], []);
5455
}
5556

56-
$classConstParts = explode('::', $constantName->getValue());
57-
if (count($classConstParts) >= 2) {
58-
$classConstName = new Node\Name\FullyQualified(ltrim($classConstParts[0], '\\'));
59-
if ($classConstName->isSpecialClassName()) {
60-
$classConstName = new Node\Name($classConstName->toString());
61-
}
62-
$constNode = new Node\Expr\ClassConstFetch(
63-
$classConstName,
64-
new Node\Identifier($classConstParts[1]),
65-
);
66-
} else {
67-
$constNode = new Node\Expr\ConstFetch(
68-
new Node\Name\FullyQualified($constantName->getValue()),
69-
);
70-
}
71-
7257
return $this->typeSpecifier->create(
73-
$constNode,
58+
$this->constantHelper->createExprFromConstantName($constantName->getValue()),
7459
new MixedType(),
7560
$context,
7661
false,

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,7 @@ public function dataFileAsserts(): iterable
642642
yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var-array.php');
643643

644644
if (PHP_VERSION_ID >= 80100) {
645+
yield from $this->gatherAssertTypes(__DIR__ . '/data/constant.php');
645646
yield from $this->gatherAssertTypes(__DIR__ . '/data/enums.php');
646647
yield from $this->gatherAssertTypes(__DIR__ . '/data/enums-import-alias.php');
647648
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7176.php');
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Constant;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
define('FOO', 'foo');
8+
const BAR = 'bar';
9+
10+
class Baz
11+
{
12+
const BAZ = 'baz';
13+
}
14+
15+
enum Suit
16+
{
17+
case Hearts;
18+
}
19+
20+
function doFoo(string $constantName): void
21+
{
22+
assertType('mixed', constant($constantName));
23+
}
24+
25+
assertType("'foo'", FOO);
26+
assertType("'foo'", constant('FOO'));
27+
assertType("*ERROR*", constant('\Constant\FOO'));
28+
29+
assertType("'bar'", BAR);
30+
assertType("*ERROR*", constant('BAR'));
31+
assertType("'bar'", constant('\Constant\BAR'));
32+
33+
assertType("'bar'|'foo'", constant(rand(0, 1) ? 'FOO' : '\Constant\BAR'));
34+
35+
assertType("'baz'", constant('\Constant\Baz::BAZ'));
36+
37+
assertType('Constant\Suit::Hearts', Suit::Hearts);
38+
assertType('Constant\Suit::Hearts', constant('\Constant\Suit::Hearts'));
39+
40+
assertType('*ERROR*', constant('UNDEFINED'));

0 commit comments

Comments
 (0)