gmarche/vendor/phpdocumentor/type-resolver/src/TypeResolver.php

471 lines
16 KiB
PHP

<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace phpDocumentor\Reflection;
use ArrayIterator;
use InvalidArgumentException;
use phpDocumentor\Reflection\Types\Array_;
use phpDocumentor\Reflection\Types\Collection;
use phpDocumentor\Reflection\Types\Compound;
use phpDocumentor\Reflection\Types\Context;
use phpDocumentor\Reflection\Types\Integer;
use phpDocumentor\Reflection\Types\Iterable_;
use phpDocumentor\Reflection\Types\Nullable;
use phpDocumentor\Reflection\Types\Object_;
use phpDocumentor\Reflection\Types\String_;
use RuntimeException;
use const PREG_SPLIT_DELIM_CAPTURE;
use const PREG_SPLIT_NO_EMPTY;
use function array_keys;
use function array_pop;
use function class_exists;
use function class_implements;
use function count;
use function in_array;
use function preg_split;
use function strlen;
use function strpos;
use function strtolower;
use function substr;
use function trim;
final class TypeResolver
{
/** @var string Definition of the ARRAY operator for types */
private const OPERATOR_ARRAY = '[]';
/** @var string Definition of the NAMESPACE operator in PHP */
private const OPERATOR_NAMESPACE = '\\';
/** @var int the iterator parser is inside a compound context */
private const PARSER_IN_COMPOUND = 0;
/** @var int the iterator parser is inside a nullable expression context */
private const PARSER_IN_NULLABLE = 1;
/** @var int the iterator parser is inside an array expression context */
private const PARSER_IN_ARRAY_EXPRESSION = 2;
/** @var int the iterator parser is inside a collection expression context */
private const PARSER_IN_COLLECTION_EXPRESSION = 3;
/**
* @var array<string, string> List of recognized keywords and unto which Value Object they map
* @psalm-var array<string, class-string<Type>>
*/
private $keywords = [
'string' => Types\String_::class,
'int' => Types\Integer::class,
'integer' => Types\Integer::class,
'bool' => Types\Boolean::class,
'boolean' => Types\Boolean::class,
'real' => Types\Float_::class,
'float' => Types\Float_::class,
'double' => Types\Float_::class,
'object' => Object_::class,
'mixed' => Types\Mixed_::class,
'array' => Array_::class,
'resource' => Types\Resource_::class,
'void' => Types\Void_::class,
'null' => Types\Null_::class,
'scalar' => Types\Scalar::class,
'callback' => Types\Callable_::class,
'callable' => Types\Callable_::class,
'false' => Types\Boolean::class,
'true' => Types\Boolean::class,
'self' => Types\Self_::class,
'$this' => Types\This::class,
'static' => Types\Static_::class,
'parent' => Types\Parent_::class,
'iterable' => Iterable_::class,
];
/** @var FqsenResolver */
private $fqsenResolver;
/**
* Initializes this TypeResolver with the means to create and resolve Fqsen objects.
*/
public function __construct(?FqsenResolver $fqsenResolver = null)
{
$this->fqsenResolver = $fqsenResolver ?: new FqsenResolver();
}
/**
* Analyzes the given type and returns the FQCN variant.
*
* When a type is provided this method checks whether it is not a keyword or
* Fully Qualified Class Name. If so it will use the given namespace and
* aliases to expand the type to a FQCN representation.
*
* This method only works as expected if the namespace and aliases are set;
* no dynamic reflection is being performed here.
*
* @uses Context::getNamespaceAliases() to check whether the first part of the relative type name should not be
* replaced with another namespace.
* @uses Context::getNamespace() to determine with what to prefix the type name.
*
* @param string $type The relative or absolute type.
*/
public function resolve(string $type, ?Context $context = null) : Type
{
$type = trim($type);
if (!$type) {
throw new InvalidArgumentException('Attempted to resolve "' . $type . '" but it appears to be empty');
}
if ($context === null) {
$context = new Context('');
}
// split the type string into tokens `|`, `?`, `<`, `>`, `,`, `(`, `)[]`, '<', '>' and type names
$tokens = preg_split(
'/(\\||\\?|<|>|, ?|\\(|\\)(?:\\[\\])+)/',
$type,
-1,
PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE
);
if ($tokens === false) {
throw new InvalidArgumentException('Unable to split the type string "' . $type . '" into tokens');
}
$tokenIterator = new ArrayIterator($tokens);
return $this->parseTypes($tokenIterator, $context, self::PARSER_IN_COMPOUND);
}
/**
* Analyse each tokens and creates types
*
* @param ArrayIterator $tokens the iterator on tokens
* @param int $parserContext on of self::PARSER_* constants, indicating
* the context where we are in the parsing
*/
private function parseTypes(ArrayIterator $tokens, Context $context, int $parserContext) : Type
{
$types = [];
$token = '';
while ($tokens->valid()) {
$token = $tokens->current();
if ($token === '|') {
if (count($types) === 0) {
throw new RuntimeException(
'A type is missing before a type separator'
);
}
if ($parserContext !== self::PARSER_IN_COMPOUND
&& $parserContext !== self::PARSER_IN_ARRAY_EXPRESSION
&& $parserContext !== self::PARSER_IN_COLLECTION_EXPRESSION
) {
throw new RuntimeException(
'Unexpected type separator'
);
}
$tokens->next();
} elseif ($token === '?') {
if ($parserContext !== self::PARSER_IN_COMPOUND
&& $parserContext !== self::PARSER_IN_ARRAY_EXPRESSION
&& $parserContext !== self::PARSER_IN_COLLECTION_EXPRESSION
) {
throw new RuntimeException(
'Unexpected nullable character'
);
}
$tokens->next();
$type = $this->parseTypes($tokens, $context, self::PARSER_IN_NULLABLE);
$types[] = new Nullable($type);
} elseif ($token === '(') {
$tokens->next();
$type = $this->parseTypes($tokens, $context, self::PARSER_IN_ARRAY_EXPRESSION);
$resolvedType = new Array_($type);
$token = $tokens->current();
// Someone did not properly close their array expression ..
if ($token === null) {
break;
}
// we generate arrays corresponding to the number of '[]' after the ')'
$numberOfArrays = (strlen($token) - 1) / 2;
for ($i = 0; $i < $numberOfArrays - 1; ++$i) {
$resolvedType = new Array_($resolvedType);
}
$types[] = $resolvedType;
$tokens->next();
} elseif ($parserContext === self::PARSER_IN_ARRAY_EXPRESSION && $token[0] === ')') {
break;
} elseif ($token === '<') {
if (count($types) === 0) {
throw new RuntimeException(
'Unexpected collection operator "<", class name is missing'
);
}
$classType = array_pop($types);
if ($classType !== null) {
$types[] = $this->resolveCollection($tokens, $classType, $context);
}
$tokens->next();
} elseif ($parserContext === self::PARSER_IN_COLLECTION_EXPRESSION
&& ($token === '>' || trim($token) === ',')
) {
break;
} else {
$type = $this->resolveSingleType($token, $context);
$tokens->next();
if ($parserContext === self::PARSER_IN_NULLABLE) {
return $type;
}
$types[] = $type;
}
}
if ($token === '|') {
throw new RuntimeException(
'A type is missing after a type separator'
);
}
if (count($types) === 0) {
if ($parserContext === self::PARSER_IN_NULLABLE) {
throw new RuntimeException(
'A type is missing after a nullable character'
);
}
if ($parserContext === self::PARSER_IN_ARRAY_EXPRESSION) {
throw new RuntimeException(
'A type is missing in an array expression'
);
}
if ($parserContext === self::PARSER_IN_COLLECTION_EXPRESSION) {
throw new RuntimeException(
'A type is missing in a collection expression'
);
}
} elseif (count($types) === 1) {
return $types[0];
}
return new Compound($types);
}
/**
* resolve the given type into a type object
*
* @param string $type the type string, representing a single type
*
* @return Type|Array_|Object_
*/
private function resolveSingleType(string $type, Context $context)
{
switch (true) {
case $this->isKeyword($type):
return $this->resolveKeyword($type);
case $this->isTypedArray($type):
return $this->resolveTypedArray($type, $context);
case $this->isFqsen($type):
return $this->resolveTypedObject($type);
case $this->isPartialStructuralElementName($type):
return $this->resolveTypedObject($type, $context);
// @codeCoverageIgnoreStart
default:
// I haven't got the foggiest how the logic would come here but added this as a defense.
throw new RuntimeException(
'Unable to resolve type "' . $type . '", there is no known method to resolve it'
);
}
// @codeCoverageIgnoreEnd
}
/**
* Adds a keyword to the list of Keywords and associates it with a specific Value Object.
*/
public function addKeyword(string $keyword, string $typeClassName) : void
{
if (!class_exists($typeClassName)) {
throw new InvalidArgumentException(
'The Value Object that needs to be created with a keyword "' . $keyword . '" must be an existing class'
. ' but we could not find the class ' . $typeClassName
);
}
if (!in_array(Type::class, class_implements($typeClassName), true)) {
throw new InvalidArgumentException(
'The class "' . $typeClassName . '" must implement the interface "phpDocumentor\Reflection\Type"'
);
}
$this->keywords[$keyword] = $typeClassName;
}
/**
* Detects whether the given type represents an array.
*
* @param string $type A relative or absolute type as defined in the phpDocumentor documentation.
*/
private function isTypedArray(string $type) : bool
{
return substr($type, -2) === self::OPERATOR_ARRAY;
}
/**
* Detects whether the given type represents a PHPDoc keyword.
*
* @param string $type A relative or absolute type as defined in the phpDocumentor documentation.
*/
private function isKeyword(string $type) : bool
{
return in_array(strtolower($type), array_keys($this->keywords), true);
}
/**
* Detects whether the given type represents a relative structural element name.
*
* @param string $type A relative or absolute type as defined in the phpDocumentor documentation.
*/
private function isPartialStructuralElementName(string $type) : bool
{
return ($type[0] !== self::OPERATOR_NAMESPACE) && !$this->isKeyword($type);
}
/**
* Tests whether the given type is a Fully Qualified Structural Element Name.
*/
private function isFqsen(string $type) : bool
{
return strpos($type, self::OPERATOR_NAMESPACE) === 0;
}
/**
* Resolves the given typed array string (i.e. `string[]`) into an Array object with the right types set.
*/
private function resolveTypedArray(string $type, Context $context) : Array_
{
return new Array_($this->resolveSingleType(substr($type, 0, -2), $context));
}
/**
* Resolves the given keyword (such as `string`) into a Type object representing that keyword.
*/
private function resolveKeyword(string $type) : Type
{
$className = $this->keywords[strtolower($type)];
return new $className();
}
/**
* Resolves the given FQSEN string into an FQSEN object.
*/
private function resolveTypedObject(string $type, ?Context $context = null) : Object_
{
return new Object_($this->fqsenResolver->resolve($type, $context));
}
/**
* Resolves the collection values and keys
*
* @return Array_|Collection
*/
private function resolveCollection(ArrayIterator $tokens, Type $classType, Context $context) : Type
{
$isArray = ((string) $classType === 'array');
// allow only "array" or class name before "<"
if (!$isArray
&& (!$classType instanceof Object_ || $classType->getFqsen() === null)) {
throw new RuntimeException(
$classType . ' is not a collection'
);
}
$tokens->next();
$valueType = $this->parseTypes($tokens, $context, self::PARSER_IN_COLLECTION_EXPRESSION);
$keyType = null;
if ($tokens->current() !== null && trim($tokens->current()) === ',') {
// if we have a comma, then we just parsed the key type, not the value type
$keyType = $valueType;
if ($isArray) {
// check the key type for an "array" collection. We allow only
// strings or integers.
if (!$keyType instanceof String_ &&
!$keyType instanceof Integer &&
!$keyType instanceof Compound
) {
throw new RuntimeException(
'An array can have only integers or strings as keys'
);
}
if ($keyType instanceof Compound) {
foreach ($keyType->getIterator() as $item) {
if (!$item instanceof String_ &&
!$item instanceof Integer
) {
throw new RuntimeException(
'An array can have only integers or strings as keys'
);
}
}
}
}
$tokens->next();
// now let's parse the value type
$valueType = $this->parseTypes($tokens, $context, self::PARSER_IN_COLLECTION_EXPRESSION);
}
if ($tokens->current() !== '>') {
if (empty($tokens->current())) {
throw new RuntimeException(
'Collection: ">" is missing'
);
}
throw new RuntimeException(
'Unexpected character "' . $tokens->current() . '", ">" is missing'
);
}
if ($isArray) {
return new Array_($valueType, $keyType);
}
/** @psalm-suppress RedundantCondition */
if ($classType instanceof Object_) {
return $this->makeCollectionFromObject($classType, $valueType, $keyType);
}
throw new RuntimeException('Invalid $classType provided');
}
private function makeCollectionFromObject(Object_ $object, Type $valueType, ?Type $keyType = null) : Collection
{
return new Collection($object->getFqsen(), $valueType, $keyType);
}
}