Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recognize constants as constant types #1163

Merged
merged 9 commits into from Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions conf/config.neon
Expand Up @@ -486,6 +486,10 @@ services:

-
class: PHPStan\Analyser\ConstantResolver
factory: @PHPStan\Analyser\ConstantResolverFactory::create()

-
class: PHPStan\Analyser\ConstantResolverFactory

-
implement: PHPStan\Analyser\ResultCache\ResultCacheManagerFactory
Expand Down
25 changes: 13 additions & 12 deletions src/Analyser/ConstantResolver.php
Expand Up @@ -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;
Expand All @@ -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([
Expand Down Expand Up @@ -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);
}
Expand All @@ -264,4 +260,9 @@ public function resolveConstantType(string $constantName, Type $constantType): T
return $constantType;
}

private function getReflectionProvider(): ReflectionProvider
{
return $this->reflectionProviderProvider->getReflectionProvider();
}

}
26 changes: 26 additions & 0 deletions src/Analyser/ConstantResolverFactory.php
@@ -0,0 +1,26 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\DependencyInjection\Container;
use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider;

class ConstantResolverFactory
{

public function __construct(
private ReflectionProviderProvider $reflectionProviderProvider,
private Container $container,
)
{
}

public function create(): ConstantResolver
{
return new ConstantResolver(
$this->reflectionProviderProvider,
$this->container->getParameter('dynamicConstantNames'),
);
}

}
50 changes: 48 additions & 2 deletions src/Analyser/NameScope.php
Expand Up @@ -25,9 +25,10 @@ class NameScope
/**
* @api
* @param array<string, string> $uses alias(string) => fullName(string)
* @param array<string, string> $constUses alias(string) => fullName(string)
* @param array<string, true> $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();
}
Expand All @@ -50,6 +51,14 @@ public function hasUseAlias(string $name): bool
return isset($this->uses[strtolower($name)]);
}

/**
* @return array<string, string>
*/
public function getConstUses(): array
{
return $this->constUses;
}

public function getClassName(): ?string
{
return $this->className;
Expand Down Expand Up @@ -78,6 +87,37 @@ public function resolveStringName(string $name): string
return $name;
}

/**
* @return non-empty-list<string>
*/
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) {
Expand Down Expand Up @@ -121,6 +161,8 @@ public function withTemplateTypeMap(TemplateTypeMap $map): self
$map->getTypes(),
)),
$this->typeAliasesMap,
$this->bypassTypeAliases,
$this->constUses,
);
}

Expand All @@ -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
Expand All @@ -168,6 +212,8 @@ public static function __set_state(array $properties): self
$properties['functionName'],
$properties['templateTypeMap'],
$properties['typeAliasesMap'],
$properties['bypassTypeAliases'],
$properties['constUses'],
);
}

Expand Down
17 changes: 8 additions & 9 deletions src/Dependency/ExportedNode/ExportedPhpDocNode.php
Expand Up @@ -9,15 +9,12 @@
class ExportedPhpDocNode implements ExportedNode, JsonSerializable
{

/** @var array<string, string> alias(string) => fullName(string) */
private array $uses;

/**
* @param array<string, string> $uses
* @param array<string, string> $uses alias(string) => fullName(string)
* @param array<string, string> $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
Expand All @@ -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;
}

/**
Expand All @@ -43,6 +41,7 @@ public function jsonSerialize()
'phpDocString' => $this->phpDocString,
'namespace' => $this->namespace,
'uses' => $this->uses,
'constUses' => $this->constUses,
],
];
}
Expand All @@ -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']);
}

/**
Expand All @@ -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']);
}

}
2 changes: 1 addition & 1 deletion src/Dependency/ExportedNodeResolver.php
Expand Up @@ -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());
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/DependencyInjection/ValidateIgnoredErrorsExtension.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -81,6 +83,7 @@ public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type
}

}),
new ConstantResolver($reflectionProviderProvider, []),
),
),
);
Expand Down
30 changes: 30 additions & 0 deletions src/PhpDoc/TypeNodeResolver.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -99,6 +103,7 @@ public function __construct(
private TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider,
private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider,
private TypeAliasResolverProvider $typeAliasResolverProvider,
private ConstantResolver $constantResolver,
)
{
}
Expand Down Expand Up @@ -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)) {
Expand Down
11 changes: 4 additions & 7 deletions src/Testing/PHPStanTestCase.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -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),
);
}

Expand Down