diff --git a/conf/config.neon b/conf/config.neon index 039f1675eb..7d8804d131 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -486,6 +486,10 @@ services: - class: PHPStan\Analyser\ConstantResolver + factory: @PHPStan\Analyser\ConstantResolverFactory::create() + + - + class: PHPStan\Analyser\ConstantResolverFactory - implement: PHPStan\Analyser\ResultCache\ResultCacheManagerFactory diff --git a/src/Analyser/ConstantResolver.php b/src/Analyser/ConstantResolver.php index d7d421e73a..fea88fea51 100644 --- a/src/Analyser/ConstantResolver.php +++ b/src/Analyser/ConstantResolver.php @@ -3,8 +3,8 @@ namespace PHPStan\Analyser; use PhpParser\Node\Name; -use PHPStan\DependencyInjection\Container; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; @@ -21,25 +21,21 @@ class ConstantResolver { - /** @var string[] */ - private array $dynamicConstantNames; - - public function __construct( - private ReflectionProvider $reflectionProvider, - Container $container, - ) + /** + * @param string[] $dynamicConstantNames + */ + public function __construct(private ReflectionProviderProvider $reflectionProviderProvider, private array $dynamicConstantNames) { - $this->dynamicConstantNames = $container->getParameter('dynamicConstantNames'); } public function resolveConstant(Name $name, ?Scope $scope): ?Type { - if (!$this->reflectionProvider->hasConstant($name, $scope)) { + if (!$this->getReflectionProvider()->hasConstant($name, $scope)) { return null; } /** @var string $resolvedConstantName */ - $resolvedConstantName = $this->reflectionProvider->resolveConstantName($name, $scope); + $resolvedConstantName = $this->getReflectionProvider()->resolveConstantName($name, $scope); // core, https://www.php.net/manual/en/reserved.constants.php if ($resolvedConstantName === 'PHP_VERSION') { return new IntersectionType([ @@ -250,7 +246,7 @@ public function resolveConstant(Name $name, ?Scope $scope): ?Type return IntegerRangeType::fromInterval(1, null); } - $constantType = $this->reflectionProvider->getConstant($name, $scope)->getValueType(); + $constantType = $this->getReflectionProvider()->getConstant($name, $scope)->getValueType(); return $this->resolveConstantType($resolvedConstantName, $constantType); } @@ -264,4 +260,9 @@ public function resolveConstantType(string $constantName, Type $constantType): T return $constantType; } + private function getReflectionProvider(): ReflectionProvider + { + return $this->reflectionProviderProvider->getReflectionProvider(); + } + } diff --git a/src/Analyser/ConstantResolverFactory.php b/src/Analyser/ConstantResolverFactory.php new file mode 100644 index 0000000000..bd63830f59 --- /dev/null +++ b/src/Analyser/ConstantResolverFactory.php @@ -0,0 +1,26 @@ +reflectionProviderProvider, + $this->container->getParameter('dynamicConstantNames'), + ); + } + +} diff --git a/src/Analyser/NameScope.php b/src/Analyser/NameScope.php index 683732b28c..bb8a4347e6 100644 --- a/src/Analyser/NameScope.php +++ b/src/Analyser/NameScope.php @@ -25,9 +25,10 @@ class NameScope /** * @api * @param array $uses alias(string) => fullName(string) + * @param array $constUses alias(string) => fullName(string) * @param array $typeAliasesMap */ - public function __construct(private ?string $namespace, private array $uses, private ?string $className = null, private ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, private array $typeAliasesMap = [], private bool $bypassTypeAliases = false) + public function __construct(private ?string $namespace, private array $uses, private ?string $className = null, private ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, private array $typeAliasesMap = [], private bool $bypassTypeAliases = false, private array $constUses = []) { $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); } @@ -50,6 +51,14 @@ public function hasUseAlias(string $name): bool return isset($this->uses[strtolower($name)]); } + /** + * @return array + */ + public function getConstUses(): array + { + return $this->constUses; + } + public function getClassName(): ?string { return $this->className; @@ -78,6 +87,37 @@ public function resolveStringName(string $name): string return $name; } + /** + * @return non-empty-list + */ + public function resolveConstantNames(string $name): array + { + if (strpos($name, '\\') === 0) { + return [ltrim($name, '\\')]; + } + + $nameParts = explode('\\', $name); + $firstNamePart = strtolower($nameParts[0]); + + if (count($nameParts) > 1) { + if (isset($this->uses[$firstNamePart])) { + array_shift($nameParts); + return [sprintf('%s\\%s', $this->uses[$firstNamePart], implode('\\', $nameParts))]; + } + } elseif (isset($this->constUses[$firstNamePart])) { + return [$this->constUses[$firstNamePart]]; + } + + if ($this->namespace !== null) { + return [ + sprintf('%s\\%s', $this->namespace, $name), + $name, + ]; + } + + return [$name]; + } + public function getTemplateTypeScope(): ?TemplateTypeScope { if ($this->className !== null) { @@ -121,6 +161,8 @@ public function withTemplateTypeMap(TemplateTypeMap $map): self $map->getTypes(), )), $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, ); } @@ -138,12 +180,14 @@ public function unsetTemplateType(string $name): self $this->functionName, $this->templateTypeMap->unsetType($name), $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, ); } public function bypassTypeAliases(): self { - return new self($this->namespace, $this->uses, $this->className, $this->functionName, $this->templateTypeMap, $this->typeAliasesMap, true); + return new self($this->namespace, $this->uses, $this->className, $this->functionName, $this->templateTypeMap, $this->typeAliasesMap, true, $this->constUses); } public function shouldBypassTypeAliases(): bool @@ -168,6 +212,8 @@ public static function __set_state(array $properties): self $properties['functionName'], $properties['templateTypeMap'], $properties['typeAliasesMap'], + $properties['bypassTypeAliases'], + $properties['constUses'], ); } diff --git a/src/Dependency/ExportedNode/ExportedPhpDocNode.php b/src/Dependency/ExportedNode/ExportedPhpDocNode.php index fbb3d3bd0d..9900790413 100644 --- a/src/Dependency/ExportedNode/ExportedPhpDocNode.php +++ b/src/Dependency/ExportedNode/ExportedPhpDocNode.php @@ -9,15 +9,12 @@ class ExportedPhpDocNode implements ExportedNode, JsonSerializable { - /** @var array alias(string) => fullName(string) */ - private array $uses; - /** - * @param array $uses + * @param array $uses alias(string) => fullName(string) + * @param array $constUses alias(string) => fullName(string) */ - public function __construct(private string $phpDocString, private ?string $namespace, array $uses) + public function __construct(private string $phpDocString, private ?string $namespace, private array $uses, private array $constUses) { - $this->uses = $uses; } public function equals(ExportedNode $node): bool @@ -28,7 +25,8 @@ public function equals(ExportedNode $node): bool return $this->phpDocString === $node->phpDocString && $this->namespace === $node->namespace - && $this->uses === $node->uses; + && $this->uses === $node->uses + && $this->constUses === $node->constUses; } /** @@ -43,6 +41,7 @@ public function jsonSerialize() 'phpDocString' => $this->phpDocString, 'namespace' => $this->namespace, 'uses' => $this->uses, + 'constUses' => $this->constUses, ], ]; } @@ -53,7 +52,7 @@ public function jsonSerialize() */ public static function __set_state(array $properties): ExportedNode { - return new self($properties['phpDocString'], $properties['namespace'], $properties['uses']); + return new self($properties['phpDocString'], $properties['namespace'], $properties['uses'], $properties['constUses']); } /** @@ -62,7 +61,7 @@ public static function __set_state(array $properties): ExportedNode */ public static function decode(array $data): ExportedNode { - return new self($data['phpDocString'], $data['namespace'], $data['uses']); + return new self($data['phpDocString'], $data['namespace'], $data['uses'], $data['constUses']); } } diff --git a/src/Dependency/ExportedNodeResolver.php b/src/Dependency/ExportedNodeResolver.php index 0a2dc26633..13a43d1f1a 100644 --- a/src/Dependency/ExportedNodeResolver.php +++ b/src/Dependency/ExportedNodeResolver.php @@ -273,7 +273,7 @@ private function exportPhpDocNode( return null; } - return new ExportedPhpDocNode($text, $nameScope->getNamespace(), $nameScope->getUses()); + return new ExportedPhpDocNode($text, $nameScope->getNamespace(), $nameScope->getUses(), $nameScope->getConstUses()); } /** diff --git a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php index 260cee8f6b..9c527ec946 100644 --- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -7,6 +7,7 @@ use Nette\DI\CompilerExtension; use Nette\Utils\RegexpException; use Nette\Utils\Strings; +use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\NameScope; use PHPStan\Command\IgnoredRegexValidator; use PHPStan\PhpDoc\DirectTypeNodeResolverExtensionRegistryProvider; @@ -50,6 +51,7 @@ public function loadConfiguration(): void /** @throws void */ $parser = Llk::load(new Read('hoa://Library/Regex/Grammar.pp')); $reflectionProvider = new DummyReflectionProvider(); + $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); ReflectionProviderStaticAccessor::registerInstance($reflectionProvider); $ignoredRegexValidator = new IgnoredRegexValidator( $parser, @@ -67,7 +69,7 @@ public function getExtensions(): array }, ), - new DirectReflectionProviderProvider($reflectionProvider), + $reflectionProviderProvider, new DirectTypeAliasResolverProvider(new class implements TypeAliasResolver { public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool @@ -81,6 +83,7 @@ public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type } }), + new ConstantResolver($reflectionProviderProvider, []), ), ), ); diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index a1dfd2a34d..257d1bba53 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -7,6 +7,8 @@ use Iterator; use IteratorAggregate; use Nette\Utils\Strings; +use PhpParser\Node\Name; +use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\NameScope; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; @@ -82,10 +84,12 @@ use function array_key_exists; use function array_map; use function count; +use function explode; use function get_class; use function in_array; use function max; use function min; +use function preg_match; use function preg_quote; use function str_replace; use function strpos; @@ -99,6 +103,7 @@ public function __construct( private TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider, private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, private TypeAliasResolverProvider $typeAliasResolverProvider, + private ConstantResolver $constantResolver, ) { } @@ -361,9 +366,34 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new ErrorType(); } + if ($this->mightBeConstant($typeNode->name) && !$this->getReflectionProvider()->hasClass($stringName)) { + $constType = $this->tryResolveConstant($typeNode->name, $nameScope); + if ($constType !== null) { + return $constType; + } + } + return new ObjectType($stringName); } + private function mightBeConstant(string $name): bool + { + return preg_match('((?:^|\\\\)[A-Z_][A-Z0-9_]*$)', $name) > 0; + } + + private function tryResolveConstant(string $name, NameScope $nameScope): ?Type + { + foreach ($nameScope->resolveConstantNames($name) as $constName) { + $nameNode = new Name\FullyQualified(explode('\\', $constName)); + $constType = $this->constantResolver->resolveConstant($nameNode, null); + if ($constType !== null) { + return $constType; + } + } + + return null; + } + private function tryResolvePseudoTypeClassType(IdentifierTypeNode $typeNode, NameScope $nameScope): ?Type { if ($nameScope->hasUseAlias($typeNode->name)) { diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index 9c547e5794..6a6302479f 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -26,12 +26,12 @@ use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Type\TypeAliasResolver; use PHPStan\Type\UsefulTypeAliasResolver; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; -use ReflectionProperty; use function array_merge; use function count; use function implode; @@ -156,11 +156,8 @@ public function createScopeFactory(ReflectionProvider $reflectionProvider, TypeS { $container = self::getContainer(); - $constantResolver = new ConstantResolver($container->getByType(ReflectionProvider::class), $container); - if (count($dynamicConstantNames) > 0) { - $reflectionProperty = new ReflectionProperty(ConstantResolver::class, 'dynamicConstantNames'); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($constantResolver, $dynamicConstantNames); + if (count($dynamicConstantNames) === 0) { + $dynamicConstantNames = $container->getParameter('dynamicConstantNames'); } return new DirectScopeFactory( @@ -176,7 +173,7 @@ public function createScopeFactory(ReflectionProvider $reflectionProvider, TypeS $this->shouldTreatPhpDocTypesAsCertain(), $container->getByType(PhpVersion::class), $container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew'], - $constantResolver, + new ConstantResolver(new DirectReflectionProviderProvider($reflectionProvider), $dynamicConstantNames), ); } diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index bda38fb67d..9b9cd8ea8f 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -244,9 +244,10 @@ private function createNameScopeMap( /** @var array $functionStack */ $functionStack = []; $uses = []; + $constUses = []; $this->processNodes( $this->phpParser->parseFile($fileName), - function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$classStack, &$typeAliasStack, &$namespace, &$functionStack, &$uses, &$typeMapStack): ?int { + function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$classStack, &$typeAliasStack, &$namespace, &$functionStack, &$uses, &$typeMapStack, &$constUses): ?int { if ($node instanceof Node\Stmt\ClassLike) { if ($traitFound && $fileName === $originalClassFileName) { return self::SKIP_NODE; @@ -296,11 +297,11 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { $phpDocString = GetLastDocComment::forNode($node); if ($phpDocString !== '') { - $typeMapStack[] = function () use ($namespace, $uses, $className, $functionName, $phpDocString, $typeMapStack): TemplateTypeMap { + $typeMapStack[] = function () use ($namespace, $uses, $className, $functionName, $phpDocString, $typeMapStack, $constUses): TemplateTypeMap { $phpDocNode = $this->resolvePhpDocStringToDocNode($phpDocString); $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null; $currentTypeMap = $typeMapCb !== null ? $typeMapCb() : null; - $nameScope = new NameScope($namespace, $uses, $className, $functionName, $currentTypeMap); + $nameScope = new NameScope($namespace, $uses, $className, $functionName, $currentTypeMap, [], false, $constUses); $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope); $templateTypeScope = $nameScope->getTemplateTypeScope(); if ($templateTypeScope === null) { @@ -344,6 +345,8 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA $functionName, ($typeMapCb !== null ? $typeMapCb() : TemplateTypeMap::createEmpty()), $typeAliasesMap, + false, + $constUses, ); } @@ -358,18 +361,24 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA if ($node instanceof Node\Stmt\Namespace_) { $namespace = (string) $node->name; - } elseif ($node instanceof Node\Stmt\Use_ && $node->type === Node\Stmt\Use_::TYPE_NORMAL) { - foreach ($node->uses as $use) { - $uses[strtolower($use->getAlias()->name)] = (string) $use->name; + } elseif ($node instanceof Node\Stmt\Use_) { + if ($node->type === Node\Stmt\Use_::TYPE_NORMAL) { + foreach ($node->uses as $use) { + $uses[strtolower($use->getAlias()->name)] = (string) $use->name; + } + } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT) { + foreach ($node->uses as $use) { + $constUses[strtolower($use->getAlias()->name)] = (string) $use->name; + } } } elseif ($node instanceof Node\Stmt\GroupUse) { $prefix = (string) $node->prefix; foreach ($node->uses as $use) { - if ($node->type !== Node\Stmt\Use_::TYPE_NORMAL && $use->type !== Node\Stmt\Use_::TYPE_NORMAL) { - continue; + if ($node->type === Node\Stmt\Use_::TYPE_NORMAL || $use->type === Node\Stmt\Use_::TYPE_NORMAL) { + $uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name); + } elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT || $use->type === Node\Stmt\Use_::TYPE_CONSTANT) { + $constUses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name); } - - $uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name); } } elseif ($node instanceof Node\Stmt\TraitUse) { $traitMethodAliases = []; @@ -475,7 +484,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA return null; }, - static function (Node $node, $callbackResult) use ($lookForTrait, &$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack): void { + static function (Node $node, $callbackResult) use ($lookForTrait, &$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack, &$constUses): void { if ($node instanceof Node\Stmt\ClassLike && $lookForTrait === null) { if (count($classStack) === 0) { throw new ShouldNotHappenException(); @@ -496,6 +505,7 @@ static function (Node $node, $callbackResult) use ($lookForTrait, &$namespace, & } elseif ($node instanceof Node\Stmt\Namespace_) { $namespace = null; $uses = []; + $constUses = []; } elseif ($node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { if (count($functionStack) === 0) { throw new ShouldNotHappenException(); diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 849385560f..dc96ba9100 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -665,6 +665,16 @@ public function testBug4732(): void $this->assertNoErrors($errors); } + public function testBug6160(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6160.php'); + $this->assertCount(2, $errors); + $this->assertSame('Parameter #1 $flags of static method Bug6160\HelloWorld::split() expects 0|1|2, 94561 given.', $errors[0]->getMessage()); + $this->assertSame(19, $errors[0]->getLine()); + $this->assertSame('Parameter #1 $flags of static method Bug6160\HelloWorld::split() expects 0|1|2, \'sdf\' given.', $errors[1]->getMessage()); + $this->assertSame(23, $errors[1]->getLine()); + } + /** * @param string[]|null $allAnalysedFiles * @return Error[] diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index d4084c82a8..61f1872734 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -868,6 +868,11 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-3284.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/int-mask.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/conditional-types-constant.php'); + + require_once __DIR__ . '/data/constant-phpdoc-type.php'; + yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-phpdoc-type.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-6160.php b/tests/PHPStan/Analyser/data/bug-6160.php new file mode 100644 index 0000000000..9470109e79 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6160.php @@ -0,0 +1,25 @@ + + */ + public static function split($flags = 0){ + return []; + } + + public static function test(): void + { + self::split(94561); // should error + self::split(PREG_SPLIT_NO_EMPTY); // should work + self::split(PREG_SPLIT_DELIM_CAPTURE); // should work + self::split(PREG_SPLIT_NO_EMPTY_COPY); // should work + self::split("sdf"); // should error + } +} diff --git a/tests/PHPStan/Analyser/data/conditional-types-constant.php b/tests/PHPStan/Analyser/data/conditional-types-constant.php new file mode 100644 index 0000000000..2d19a65439 --- /dev/null +++ b/tests/PHPStan/Analyser/data/conditional-types-constant.php @@ -0,0 +1,24 @@ +returnsTrueForPREG_SPLIT_NO_EMPTY(PREG_SPLIT_NO_EMPTY)); + assertType('true', $this->returnsTrueForPREG_SPLIT_NO_EMPTY(1)); + assertType('false', $this->returnsTrueForPREG_SPLIT_NO_EMPTY(PREG_SPLIT_OFFSET_CAPTURE)); + assertType('false', $this->returnsTrueForPREG_SPLIT_NO_EMPTY(4)); + assertType('bool', $this->returnsTrueForPREG_SPLIT_NO_EMPTY($_GET['flag'])); + } +} diff --git a/tests/PHPStan/Analyser/data/constant-phpdoc-type.php b/tests/PHPStan/Analyser/data/constant-phpdoc-type.php new file mode 100644 index 0000000000..cc739c1d5e --- /dev/null +++ b/tests/PHPStan/Analyser/data/constant-phpdoc-type.php @@ -0,0 +1,55 @@ +