Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -1333,7 +1333,11 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc

if ($constExpr instanceof ConstFetchNode) {
if ($constExpr->className === '') {
throw new ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode
if (!Strings::contains($constExpr->name, '*')) {
throw new ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode
}

return $this->resolveGlobalConstantPattern($constExpr->name, $nameScope);
}

$isStatic = false;
Expand Down Expand Up @@ -1476,6 +1480,41 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc
return new ErrorType();
}

/**
* Resolves a pattern like PHP_ROUND_* to the union of the global constants it matches. Like PHP does
* for an unqualified constant name, an unqualified pattern looks in the current namespace first and
* falls back to the global one.
*/
private function resolveGlobalConstantPattern(string $pattern, NameScope $nameScope): Type
{
$namespace = $nameScope->getNamespace();
if (Strings::contains($pattern, '\\')) {
$patterns = [$nameScope->resolveStringName($pattern)];
} elseif ($namespace !== null) {
$patterns = [$namespace . '\\' . $pattern, $pattern];
} else {
$patterns = [$pattern];
}

foreach ($patterns as $fullyQualifiedPattern) {
$constantTypes = [];
foreach ($this->getReflectionProvider()->findConstantNames($fullyQualifiedPattern) as $constantName) {
$constantType = $this->constantResolver->resolveConstant(new Name\FullyQualified($constantName), null);
if ($constantType === null) {
continue;
}

$constantTypes[] = $constantType;
}

if (count($constantTypes) > 0) {
return TypeCombinator::union(...$constantTypes);
}
}

return new ErrorType();
}

private function resolveOffsetAccessNode(OffsetAccessTypeNode $typeNode, NameScope $nameScope): Type
{
$type = $this->resolve($typeNode->type, $nameScope);
Expand Down
60 changes: 60 additions & 0 deletions src/Reflection/BetterReflection/BetterReflectionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Reflection\BetterReflection;

use Closure;
use JetBrains\PHPStormStub\PhpStormStubsMap;
use Nette\Utils\Strings;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
Expand Down Expand Up @@ -39,6 +40,7 @@
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ClassReflectionFactory;
use PHPStan\Reflection\Constant\RuntimeConstantReflection;
use PHPStan\Reflection\ConstantNameHelper;
use PHPStan\Reflection\ConstantReflection;
use PHPStan\Reflection\Deprecation\DeprecationProvider;
use PHPStan\Reflection\FunctionReflection;
Expand All @@ -61,9 +63,14 @@
use PHPStan\Type\VerbosityLevel;
use function array_key_exists;
use function array_key_first;
use function array_keys;
use function array_map;
use function array_values;
use function get_defined_constants;
use function in_array;
use function preg_quote;
use function sprintf;
use function str_replace;
use function strtolower;
use const PHP_VERSION_ID;

Expand All @@ -90,6 +97,9 @@ final class BetterReflectionProvider implements ReflectionProvider
/** @var array<string, array<string, ConstantReflection>> */
private array $cachedConstants = [];

/** @var list<non-empty-string>|null */
private ?array $constantNameCandidates = null;

/**
* @param list<string> $universalObjectCratesClasses
*/
Expand Down Expand Up @@ -526,6 +536,56 @@ public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $nam
return $resolved;
}

public function findConstantNames(string $pattern): array
{
$regex = '{^' . str_replace('\\*', '.*', preg_quote(ConstantNameHelper::normalize($pattern))) . '$}D';
$names = [];
foreach ($this->getConstantNameCandidates() as $candidate) {
if (Strings::match($candidate, $regex) === null) {
continue;
}

// a candidate can be a stub of a constant the configured PHP version does not have
if (!$this->hasConstant(new Node\Name\FullyQualified($candidate), null)) {
continue;
}

$names[] = $candidate;
}

return $names;
}

/**
* Names of every global constant the reflection might find: the built-in ones from the stubs,
* the ones defined at runtime, and the ones declared in the analysed and scanned files.
*
* @return list<non-empty-string>
*/
private function getConstantNameCandidates(): array
{
if ($this->constantNameCandidates !== null) {
return $this->constantNameCandidates;
}

$names = [...array_keys(PhpStormStubsMap::CONSTANTS), ...array_keys(get_defined_constants())];
foreach ($this->reflector->reflectAllConstants() as $constantReflection) {
$names[] = $constantReflection->getName();
}

$candidates = [];
foreach ($names as $name) {
$name = ConstantNameHelper::normalize($name);
if ($name === '') {
continue; // define('', ...) is allowed, but no pattern can name it
}

$candidates[$name] = $name;
}

return $this->constantNameCandidates = array_values($candidates);
}

private function nameResolutionCacheKey(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): string
{
return ($namespaceAnswerer !== null ? ($namespaceAnswerer->getNamespace() ?? '') : '')
Expand Down
7 changes: 7 additions & 0 deletions src/Reflection/ReflectionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,11 @@ public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAn

public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAnswerer): ?string;

/**
* Names of the global constants whose fully qualified name matches $pattern, in which * stands for any sequence of characters.
*
* @return list<non-empty-string>
*/
public function findConstantNames(string $pattern): array;

}
5 changes: 5 additions & 0 deletions src/Reflection/ReflectionProvider/DummyReflectionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,9 @@ public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $nam
return null;
}

public function findConstantNames(string $pattern): array
{
return [];
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,9 @@ public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $nam
return $this->provider->resolveConstantName($nameNode, $namespaceAnswerer);
}

public function findConstantNames(string $pattern): array
{
return $this->provider->findConstantNames($pattern);
}

}
5 changes: 5 additions & 0 deletions tests/PHPStan/Dependency/CountingReflectionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,9 @@ public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $nam
return $this->inner->resolveConstantName($nameNode, $namespaceAnswerer);
}

public function findConstantNames(string $pattern): array
{
return $this->inner->findConstantNames($pattern);
}

}
52 changes: 52 additions & 0 deletions tests/PHPStan/PhpDoc/TypeNodeResolverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDoc;

use PHPStan\Analyser\NameScope;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\Testing\PHPStanTestCase;
use PHPStan\Type\VerbosityLevel;
use PHPUnit\Framework\Attributes\DataProvider;

class TypeNodeResolverTest extends PHPStanTestCase
{

/**
* @return iterable<string, array{string, non-empty-string|null, array<string, string>, string}>
*/
public static function dataGlobalConstantPattern(): iterable
{
yield 'built-in' => ['PHP_ROUND_*', null, [], '1|2|3|4'];
yield 'asterisk in the middle' => ['E_*_ERROR', null, [], '16|64|256|4096'];
yield 'fully qualified in a namespace' => ['\\PHP_ROUND_*', 'GlobalConstantWildcard', [], '1|2|3|4'];
yield 'falls back to the global namespace' => ['PHP_ROUND_*', 'GlobalConstantWildcard', [], '1|2|3|4'];
yield 'current namespace' => ['FLAG_*', 'GlobalConstantWildcard', [], "'read'|'write'"];
yield 'current namespace before the global one' => ['PHP_ROUND_*', 'GlobalConstantWildcard\\Sub', [], "'shadowed'"];
yield 'fully qualified namespaced' => ['\\GlobalConstantWildcard\\FLAG_*', null, [], "'read'|'write'"];
yield 'qualified, relative to the namespace' => ['Sub\\FLAG_*', 'GlobalConstantWildcard', [], "'execute'"];
yield 'qualified, through a use' => ['S\\FLAG_*', 'Other', ['s' => 'GlobalConstantWildcard\\Sub'], "'execute'"];
yield 'defined' => ['GLOBAL_CONSTANT_WILDCARD_*', null, [], '10|100'];
yield 'no match' => ['NOPE_*', null, [], '*ERROR*'];
yield 'no global match for a namespaced constant' => ['\\FLAG_*', 'GlobalConstantWildcard', [], '*ERROR*'];
}

/**
* @param non-empty-string|null $namespace
* @param array<string, string> $uses
*/
#[DataProvider('dataGlobalConstantPattern')]
public function testGlobalConstantPattern(string $pattern, ?string $namespace, array $uses, string $expectedType): void
{
$typeNodeResolver = self::getContainer()->getByType(TypeNodeResolver::class);

// the node phpstan/phpdoc-parser reads @param PHP_ROUND_* $mode into
$type = $typeNodeResolver->resolve(
new ConstTypeNode(new ConstFetchNode('', $pattern)),
new NameScope($namespace, $uses),
);

$this->assertSame($expectedType, $type->describe(VerbosityLevel::precise()));
}

}
19 changes: 19 additions & 0 deletions tests/PHPStan/PhpDoc/data/global-constant-wildcard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace GlobalConstantWildcard {

const FLAG_READ = 'read';
const FLAG_WRITE = 'write';
const OTHER_FLAG = 'other';

define('GLOBAL_CONSTANT_WILDCARD_LOW', 10);
define('GLOBAL_CONSTANT_WILDCARD_HIGH', 100);

}

namespace GlobalConstantWildcard\Sub {

const PHP_ROUND_SHADOWED = 'shadowed';
const FLAG_EXECUTE = 'execute';

}
Loading