Skip to content

Commit

Permalink
Recognize constants as constant types
Browse files Browse the repository at this point in the history
  • Loading branch information
rvanvelzen committed Apr 8, 2022
1 parent b0ae408 commit f0bfd4d
Show file tree
Hide file tree
Showing 15 changed files with 278 additions and 43 deletions.
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

0 comments on commit f0bfd4d

Please sign in to comment.