From db4d4925be947a89d75af4aac93f186bfc6ad599 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Thu, 7 Apr 2022 17:51:58 +0200 Subject: [PATCH] Support const aliases properly --- conf/config.neon | 2 ++ src/Analyser/ConstantResolver.php | 27 ++++++++++------ src/Analyser/ConstantResolverProvider.php | 13 ++------ src/Analyser/ContainerConstantResolver.php | 24 ++++++++++++++ src/Analyser/DirectConstantResolver.php | 26 +++++++++++++++ .../DirectConstantResolverProvider.php | 17 ++++++++++ src/Analyser/LazyConstantResolverProvider.php | 19 +++++++++++ src/Analyser/NameScope.php | 24 ++++++++++++-- .../ExportedNode/ExportedPhpDocNode.php | 17 +++++----- src/Dependency/ExportedNodeResolver.php | 2 +- .../ValidateIgnoredErrorsExtension.php | 3 ++ src/PhpDoc/TypeNodeResolver.php | 2 -- src/Testing/PHPStanTestCase.php | 11 +++---- src/Type/FileTypeMapper.php | 32 ++++++++++++------- .../Analyser/data/constant-phpdoc-type.php | 2 +- 15 files changed, 168 insertions(+), 53 deletions(-) create mode 100644 src/Analyser/ContainerConstantResolver.php create mode 100644 src/Analyser/DirectConstantResolver.php create mode 100644 src/Analyser/DirectConstantResolverProvider.php create mode 100644 src/Analyser/LazyConstantResolverProvider.php diff --git a/conf/config.neon b/conf/config.neon index 0b12da0ad6b..d2efc45d08f 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -486,9 +486,11 @@ services: - class: PHPStan\Analyser\ConstantResolver + factory: PHPStan\Analyser\ContainerConstantResolver - class: PHPStan\Analyser\ConstantResolverProvider + factory: PHPStan\Analyser\LazyConstantResolverProvider - implement: PHPStan\Analyser\ResultCache\ResultCacheManagerFactory diff --git a/src/Analyser/ConstantResolver.php b/src/Analyser/ConstantResolver.php index d7d421e73a1..59ebba72661 100644 --- a/src/Analyser/ConstantResolver.php +++ b/src/Analyser/ConstantResolver.php @@ -3,7 +3,6 @@ namespace PHPStan\Analyser; use PhpParser\Node\Name; -use PHPStan\DependencyInjection\Container; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -18,18 +17,14 @@ use function in_array; use const PHP_INT_SIZE; -class ConstantResolver +abstract class ConstantResolver { /** @var string[] */ - private array $dynamicConstantNames; + private ?array $dynamicConstantNames = null; - public function __construct( - private ReflectionProvider $reflectionProvider, - Container $container, - ) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->dynamicConstantNames = $container->getParameter('dynamicConstantNames'); } public function resolveConstant(Name $name, ?Scope $scope): ?Type @@ -257,11 +252,25 @@ public function resolveConstant(Name $name, ?Scope $scope): ?Type public function resolveConstantType(string $constantName, Type $constantType): Type { - if ($constantType instanceof ConstantType && in_array($constantName, $this->dynamicConstantNames, true)) { + if ($constantType instanceof ConstantType && $this->isDynamicConstantName($constantName)) { return $constantType->generalize(GeneralizePrecision::lessSpecific()); } return $constantType; } + private function isDynamicConstantName(string $constantName): bool + { + if ($this->dynamicConstantNames === null) { + $this->dynamicConstantNames = $this->getDynamicConstantNames(); + } + + return in_array($constantName, $this->dynamicConstantNames, true); + } + + /** + * @return string[] + */ + abstract protected function getDynamicConstantNames(): array; + } diff --git a/src/Analyser/ConstantResolverProvider.php b/src/Analyser/ConstantResolverProvider.php index 1db8f90a78e..fa0a061a1dd 100644 --- a/src/Analyser/ConstantResolverProvider.php +++ b/src/Analyser/ConstantResolverProvider.php @@ -2,18 +2,9 @@ namespace PHPStan\Analyser; -use PHPStan\DependencyInjection\Container; - -class ConstantResolverProvider +interface ConstantResolverProvider { - public function __construct(private Container $container) - { - } - - public function getConstantResolver(): ConstantResolver - { - return $this->container->getByType(ConstantResolver::class); - } + public function getConstantResolver(): ConstantResolver; } diff --git a/src/Analyser/ContainerConstantResolver.php b/src/Analyser/ContainerConstantResolver.php new file mode 100644 index 00000000000..e6bbbc63008 --- /dev/null +++ b/src/Analyser/ContainerConstantResolver.php @@ -0,0 +1,24 @@ +container->getParameter('dynamicConstantNames'); + } + +} diff --git a/src/Analyser/DirectConstantResolver.php b/src/Analyser/DirectConstantResolver.php new file mode 100644 index 00000000000..01c78ef20c2 --- /dev/null +++ b/src/Analyser/DirectConstantResolver.php @@ -0,0 +1,26 @@ +dynamicConstantNames; + } + +} diff --git a/src/Analyser/DirectConstantResolverProvider.php b/src/Analyser/DirectConstantResolverProvider.php new file mode 100644 index 00000000000..cc620e7aadc --- /dev/null +++ b/src/Analyser/DirectConstantResolverProvider.php @@ -0,0 +1,17 @@ +constantResolver; + } + +} diff --git a/src/Analyser/LazyConstantResolverProvider.php b/src/Analyser/LazyConstantResolverProvider.php new file mode 100644 index 00000000000..1325ab61020 --- /dev/null +++ b/src/Analyser/LazyConstantResolverProvider.php @@ -0,0 +1,19 @@ +container->getByType(ConstantResolver::class); + } + +} diff --git a/src/Analyser/NameScope.php b/src/Analyser/NameScope.php index cc85e571a1f..bb8a4347e69 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; @@ -88,12 +97,15 @@ public function resolveConstantNames(string $name): array } $nameParts = explode('\\', $name); + $firstNamePart = strtolower($nameParts[0]); + if (count($nameParts) > 1) { - $firstNamePart = strtolower($nameParts[0]); 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) { @@ -149,6 +161,8 @@ public function withTemplateTypeMap(TemplateTypeMap $map): self $map->getTypes(), )), $this->typeAliasesMap, + $this->bypassTypeAliases, + $this->constUses, ); } @@ -166,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 @@ -196,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 fbb3d3bd0d5..99007904132 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 0a2dc26633b..13a43d1f1ac 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 260cee8f6bf..34b78e9104f 100644 --- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -7,6 +7,8 @@ use Nette\DI\CompilerExtension; use Nette\Utils\RegexpException; use Nette\Utils\Strings; +use PHPStan\Analyser\DirectConstantResolver; +use PHPStan\Analyser\DirectConstantResolverProvider; use PHPStan\Analyser\NameScope; use PHPStan\Command\IgnoredRegexValidator; use PHPStan\PhpDoc\DirectTypeNodeResolverExtensionRegistryProvider; @@ -81,6 +83,7 @@ public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type } }), + new DirectConstantResolverProvider(new DirectConstantResolver($reflectionProvider, [])), ), ), ); diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index de923b06410..001eba96e76 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -11,7 +11,6 @@ use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\ConstantResolverProvider; use PHPStan\Analyser\NameScope; -use PHPStan\Analyser\ScopeFactory; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; @@ -106,7 +105,6 @@ public function __construct( private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, private TypeAliasResolverProvider $typeAliasResolverProvider, private ConstantResolverProvider $constantResolverProvider, - private ScopeFactory $scopeFactory, ) { } diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php index 9c547e57941..4408d92ad88 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -3,7 +3,8 @@ namespace PHPStan\Testing; use PhpParser\PrettyPrinter\Standard; -use PHPStan\Analyser\ConstantResolver; +use PHPStan\Analyser\ContainerConstantResolver; +use PHPStan\Analyser\DirectConstantResolver; use PHPStan\Analyser\DirectScopeFactory; use PHPStan\Analyser\Error; use PHPStan\Analyser\MutatingScope; @@ -31,7 +32,6 @@ 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,10 @@ 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); + $constantResolver = new DirectConstantResolver($reflectionProvider, $dynamicConstantNames); + } else { + $constantResolver = new ContainerConstantResolver($reflectionProvider, $container); } return new DirectScopeFactory( diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index bda38fb67d9..9b9cd8ea8fd 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/data/constant-phpdoc-type.php b/tests/PHPStan/Analyser/data/constant-phpdoc-type.php index eec4142d0b9..cc739c1d5eb 100644 --- a/tests/PHPStan/Analyser/data/constant-phpdoc-type.php +++ b/tests/PHPStan/Analyser/data/constant-phpdoc-type.php @@ -43,7 +43,7 @@ function foo( assertType('1', $three); assertType('1|2', $four); assertType('1', $five); - assertType('ConstantPhpdocType\PREG_SPLIT_NO_EMPTY_ALIAS', $six); // use const not supported + assertType('1', $six); assertType('ConstantPhpdocType\BAR', $seven); // classes take precedence over constants assertType("'foo'", $eight); assertType("'foo'", $nine);