diff --git a/conf/config.neon b/conf/config.neon index f12bb46d24..0b335eea79 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1363,6 +1363,14 @@ services: arguments: checkMaybeUndefinedVariables: %checkMaybeUndefinedVariables% + - + class: PHPStan\Type\Php\ConstantFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ConstantHelper + - class: PHPStan\Type\Php\CountFunctionReturnTypeExtension tags: diff --git a/src/Type/Php/ConstantFunctionReturnTypeExtension.php b/src/Type/Php/ConstantFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..3c32b6c360 --- /dev/null +++ b/src/Type/Php/ConstantFunctionReturnTypeExtension.php @@ -0,0 +1,49 @@ +getName() === 'constant'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $nameType = $scope->getType($functionCall->getArgs()[0]->value); + + $results = []; + foreach ($nameType->getConstantStrings() as $constantName) { + $results[] = $scope->getType($this->constantHelper->createExprFromConstantName($constantName->getValue())); + } + + if (count($results) > 0) { + return TypeCombinator::union(...$results); + } + + return null; + } + +} diff --git a/src/Type/Php/ConstantHelper.php b/src/Type/Php/ConstantHelper.php new file mode 100644 index 0000000000..790f169ed9 --- /dev/null +++ b/src/Type/Php/ConstantHelper.php @@ -0,0 +1,33 @@ += 2) { + $classConstName = new FullyQualified(ltrim($classConstParts[0], '\\')); + if ($classConstName->isSpecialClassName()) { + $classConstName = new Name($classConstName->toString()); + } + + return new ClassConstFetch($classConstName, new Identifier($classConstParts[1])); + } + + return new ConstFetch(new FullyQualified($constantName)); + } + +} diff --git a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php index cbc7fd13f6..43ab4c48db 100644 --- a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php +++ b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php @@ -2,7 +2,6 @@ namespace PHPStan\Type\Php; -use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; @@ -14,14 +13,16 @@ use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\MixedType; use function count; -use function explode; -use function ltrim; class DefinedConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; + public function __construct(private ConstantHelper $constantHelper) + { + } + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { $this->typeSpecifier = $typeSpecifier; @@ -53,24 +54,8 @@ public function specifyTypes( return new SpecifiedTypes([], []); } - $classConstParts = explode('::', $constantName->getValue()); - if (count($classConstParts) >= 2) { - $classConstName = new Node\Name\FullyQualified(ltrim($classConstParts[0], '\\')); - if ($classConstName->isSpecialClassName()) { - $classConstName = new Node\Name($classConstName->toString()); - } - $constNode = new Node\Expr\ClassConstFetch( - $classConstName, - new Node\Identifier($classConstParts[1]), - ); - } else { - $constNode = new Node\Expr\ConstFetch( - new Node\Name\FullyQualified($constantName->getValue()), - ); - } - return $this->typeSpecifier->create( - $constNode, + $this->constantHelper->createExprFromConstantName($constantName->getValue()), new MixedType(), $context, false, diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 271ecf21e5..9cb612eea0 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -642,6 +642,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var-array.php'); if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/constant.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/enums.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/enums-import-alias.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7176.php'); diff --git a/tests/PHPStan/Analyser/data/constant.php b/tests/PHPStan/Analyser/data/constant.php new file mode 100644 index 0000000000..c26edcdd53 --- /dev/null +++ b/tests/PHPStan/Analyser/data/constant.php @@ -0,0 +1,40 @@ += 8.1 + +namespace Constant; + +use function PHPStan\Testing\assertType; + +define('FOO', 'foo'); +const BAR = 'bar'; + +class Baz +{ + const BAZ = 'baz'; +} + +enum Suit +{ + case Hearts; +} + +function doFoo(string $constantName): void +{ + assertType('mixed', constant($constantName)); +} + +assertType("'foo'", FOO); +assertType("'foo'", constant('FOO')); +assertType("*ERROR*", constant('\Constant\FOO')); + +assertType("'bar'", BAR); +assertType("*ERROR*", constant('BAR')); +assertType("'bar'", constant('\Constant\BAR')); + +assertType("'bar'|'foo'", constant(rand(0, 1) ? 'FOO' : '\Constant\BAR')); + +assertType("'baz'", constant('\Constant\Baz::BAZ')); + +assertType('Constant\Suit::Hearts', Suit::Hearts); +assertType('Constant\Suit::Hearts', constant('\Constant\Suit::Hearts')); + +assertType('*ERROR*', constant('UNDEFINED'));