/vendor/phpdocumentor/type-resolver/src/Types/ContextFactory.php
PHP | 420 lines | 334 code | 35 blank | 51 comment | 19 complexity | c1358182b3d367934cb707aceb98991d MD5 | raw file
- <?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\Types;
- use ArrayIterator;
- use InvalidArgumentException;
- use ReflectionClass;
- use ReflectionClassConstant;
- use ReflectionMethod;
- use ReflectionParameter;
- use ReflectionProperty;
- use Reflector;
- use RuntimeException;
- use UnexpectedValueException;
- use function define;
- use function defined;
- use function file_exists;
- use function file_get_contents;
- use function get_class;
- use function in_array;
- use function is_string;
- use function strrpos;
- use function substr;
- use function token_get_all;
- use function trim;
- use const T_AS;
- use const T_CLASS;
- use const T_CURLY_OPEN;
- use const T_DOLLAR_OPEN_CURLY_BRACES;
- use const T_NAME_FULLY_QUALIFIED;
- use const T_NAME_QUALIFIED;
- use const T_NAMESPACE;
- use const T_NS_SEPARATOR;
- use const T_STRING;
- use const T_USE;
- if (!defined('T_NAME_QUALIFIED')) {
- define('T_NAME_QUALIFIED', 'T_NAME_QUALIFIED');
- }
- if (!defined('T_NAME_FULLY_QUALIFIED')) {
- define('T_NAME_FULLY_QUALIFIED', 'T_NAME_FULLY_QUALIFIED');
- }
- /**
- * Convenience class to create a Context for DocBlocks when not using the Reflection Component of phpDocumentor.
- *
- * For a DocBlock to be able to resolve types that use partial namespace names or rely on namespace imports we need to
- * provide a bit of context so that the DocBlock can read that and based on it decide how to resolve the types to
- * Fully Qualified names.
- *
- * @see Context for more information.
- */
- final class ContextFactory
- {
- /** The literal used at the end of a use statement. */
- private const T_LITERAL_END_OF_USE = ';';
- /** The literal used between sets of use statements */
- private const T_LITERAL_USE_SEPARATOR = ',';
- /**
- * Build a Context given a Class Reflection.
- *
- * @see Context for more information on Contexts.
- */
- public function createFromReflector(Reflector $reflector): Context
- {
- if ($reflector instanceof ReflectionClass) {
- //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable
- /** @var ReflectionClass<object> $reflector */
- return $this->createFromReflectionClass($reflector);
- }
- if ($reflector instanceof ReflectionParameter) {
- return $this->createFromReflectionParameter($reflector);
- }
- if ($reflector instanceof ReflectionMethod) {
- return $this->createFromReflectionMethod($reflector);
- }
- if ($reflector instanceof ReflectionProperty) {
- return $this->createFromReflectionProperty($reflector);
- }
- if ($reflector instanceof ReflectionClassConstant) {
- return $this->createFromReflectionClassConstant($reflector);
- }
- throw new UnexpectedValueException('Unhandled \Reflector instance given: ' . get_class($reflector));
- }
- private function createFromReflectionParameter(ReflectionParameter $parameter): Context
- {
- $class = $parameter->getDeclaringClass();
- if (!$class) {
- throw new InvalidArgumentException('Unable to get class of ' . $parameter->getName());
- }
- return $this->createFromReflectionClass($class);
- }
- private function createFromReflectionMethod(ReflectionMethod $method): Context
- {
- $class = $method->getDeclaringClass();
- return $this->createFromReflectionClass($class);
- }
- private function createFromReflectionProperty(ReflectionProperty $property): Context
- {
- $class = $property->getDeclaringClass();
- return $this->createFromReflectionClass($class);
- }
- private function createFromReflectionClassConstant(ReflectionClassConstant $constant): Context
- {
- //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable
- /** @phpstan-var ReflectionClass<object> $class */
- $class = $constant->getDeclaringClass();
- return $this->createFromReflectionClass($class);
- }
- /**
- * @phpstan-param ReflectionClass<object> $class
- */
- private function createFromReflectionClass(ReflectionClass $class): Context
- {
- $fileName = $class->getFileName();
- $namespace = $class->getNamespaceName();
- if (is_string($fileName) && file_exists($fileName)) {
- $contents = file_get_contents($fileName);
- if ($contents === false) {
- throw new RuntimeException('Unable to read file "' . $fileName . '"');
- }
- return $this->createForNamespace($namespace, $contents);
- }
- return new Context($namespace, []);
- }
- /**
- * Build a Context for a namespace in the provided file contents.
- *
- * @see Context for more information on Contexts.
- *
- * @param string $namespace It does not matter if a `\` precedes the namespace name,
- * this method first normalizes.
- * @param string $fileContents The file's contents to retrieve the aliases from with the given namespace.
- */
- public function createForNamespace(string $namespace, string $fileContents): Context
- {
- $namespace = trim($namespace, '\\');
- $useStatements = [];
- $currentNamespace = '';
- $tokens = new ArrayIterator(token_get_all($fileContents));
- while ($tokens->valid()) {
- $currentToken = $tokens->current();
- switch ($currentToken[0]) {
- case T_NAMESPACE:
- $currentNamespace = $this->parseNamespace($tokens);
- break;
- case T_CLASS:
- // Fast-forward the iterator through the class so that any
- // T_USE tokens found within are skipped - these are not
- // valid namespace use statements so should be ignored.
- $braceLevel = 0;
- $firstBraceFound = false;
- while ($tokens->valid() && ($braceLevel > 0 || !$firstBraceFound)) {
- $currentToken = $tokens->current();
- if (
- $currentToken === '{'
- || in_array($currentToken[0], [T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES], true)
- ) {
- if (!$firstBraceFound) {
- $firstBraceFound = true;
- }
- ++$braceLevel;
- }
- if ($currentToken === '}') {
- --$braceLevel;
- }
- $tokens->next();
- }
- break;
- case T_USE:
- if ($currentNamespace === $namespace) {
- $useStatements += $this->parseUseStatement($tokens);
- }
- break;
- }
- $tokens->next();
- }
- return new Context($namespace, $useStatements);
- }
- /**
- * Deduce the name from tokens when we are at the T_NAMESPACE token.
- *
- * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens
- */
- private function parseNamespace(ArrayIterator $tokens): string
- {
- // skip to the first string or namespace separator
- $this->skipToNextStringOrNamespaceSeparator($tokens);
- $name = '';
- $acceptedTokens = [T_STRING, T_NS_SEPARATOR, T_NAME_QUALIFIED];
- while ($tokens->valid() && in_array($tokens->current()[0], $acceptedTokens, true)) {
- $name .= $tokens->current()[1];
- $tokens->next();
- }
- return $name;
- }
- /**
- * Deduce the names of all imports when we are at the T_USE token.
- *
- * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens
- *
- * @return string[]
- * @psalm-return array<string, string>
- */
- private function parseUseStatement(ArrayIterator $tokens): array
- {
- $uses = [];
- while ($tokens->valid()) {
- $this->skipToNextStringOrNamespaceSeparator($tokens);
- $uses += $this->extractUseStatements($tokens);
- $currentToken = $tokens->current();
- if ($currentToken[0] === self::T_LITERAL_END_OF_USE) {
- return $uses;
- }
- }
- return $uses;
- }
- /**
- * Fast-forwards the iterator as longs as we don't encounter a T_STRING or T_NS_SEPARATOR token.
- *
- * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens
- */
- private function skipToNextStringOrNamespaceSeparator(ArrayIterator $tokens): void
- {
- while ($tokens->valid()) {
- $currentToken = $tokens->current();
- if (in_array($currentToken[0], [T_STRING, T_NS_SEPARATOR], true)) {
- break;
- }
- if ($currentToken[0] === T_NAME_QUALIFIED) {
- break;
- }
- if (defined('T_NAME_FULLY_QUALIFIED') && $currentToken[0] === T_NAME_FULLY_QUALIFIED) {
- break;
- }
- $tokens->next();
- }
- }
- /**
- * Deduce the namespace name and alias of an import when we are at the T_USE token or have not reached the end of
- * a USE statement yet. This will return a key/value array of the alias => namespace.
- *
- * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens
- *
- * @return string[]
- * @psalm-return array<string, string>
- *
- * @psalm-suppress TypeDoesNotContainType
- */
- private function extractUseStatements(ArrayIterator $tokens): array
- {
- $extractedUseStatements = [];
- $groupedNs = '';
- $currentNs = '';
- $currentAlias = '';
- $state = 'start';
- while ($tokens->valid()) {
- $currentToken = $tokens->current();
- $tokenId = is_string($currentToken) ? $currentToken : $currentToken[0];
- $tokenValue = is_string($currentToken) ? null : $currentToken[1];
- switch ($state) {
- case 'start':
- switch ($tokenId) {
- case T_STRING:
- case T_NS_SEPARATOR:
- $currentNs .= (string) $tokenValue;
- $currentAlias = $tokenValue;
- break;
- case T_NAME_QUALIFIED:
- case T_NAME_FULLY_QUALIFIED:
- $currentNs .= (string) $tokenValue;
- $currentAlias = substr(
- (string) $tokenValue,
- (int) (strrpos((string) $tokenValue, '\\')) + 1
- );
- break;
- case T_CURLY_OPEN:
- case '{':
- $state = 'grouped';
- $groupedNs = $currentNs;
- break;
- case T_AS:
- $state = 'start-alias';
- break;
- case self::T_LITERAL_USE_SEPARATOR:
- case self::T_LITERAL_END_OF_USE:
- $state = 'end';
- break;
- default:
- break;
- }
- break;
- case 'start-alias':
- switch ($tokenId) {
- case T_STRING:
- $currentAlias = $tokenValue;
- break;
- case self::T_LITERAL_USE_SEPARATOR:
- case self::T_LITERAL_END_OF_USE:
- $state = 'end';
- break;
- default:
- break;
- }
- break;
- case 'grouped':
- switch ($tokenId) {
- case T_STRING:
- case T_NS_SEPARATOR:
- $currentNs .= (string) $tokenValue;
- $currentAlias = $tokenValue;
- break;
- case T_AS:
- $state = 'grouped-alias';
- break;
- case self::T_LITERAL_USE_SEPARATOR:
- $state = 'grouped';
- $extractedUseStatements[(string) $currentAlias] = $currentNs;
- $currentNs = $groupedNs;
- $currentAlias = '';
- break;
- case self::T_LITERAL_END_OF_USE:
- $state = 'end';
- break;
- default:
- break;
- }
- break;
- case 'grouped-alias':
- switch ($tokenId) {
- case T_STRING:
- $currentAlias = $tokenValue;
- break;
- case self::T_LITERAL_USE_SEPARATOR:
- $state = 'grouped';
- $extractedUseStatements[(string) $currentAlias] = $currentNs;
- $currentNs = $groupedNs;
- $currentAlias = '';
- break;
- case self::T_LITERAL_END_OF_USE:
- $state = 'end';
- break;
- default:
- break;
- }
- }
- if ($state === 'end') {
- break;
- }
- $tokens->next();
- }
- if ($groupedNs !== $currentNs) {
- $extractedUseStatements[(string) $currentAlias] = $currentNs;
- }
- return $extractedUseStatements;
- }
- }