summaryrefslogtreecommitdiff
path: root/vendor/phpdocumentor/type-resolver/src/Types/ContextFactory.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/phpdocumentor/type-resolver/src/Types/ContextFactory.php')
-rw-r--r--vendor/phpdocumentor/type-resolver/src/Types/ContextFactory.php420
1 files changed, 420 insertions, 0 deletions
diff --git a/vendor/phpdocumentor/type-resolver/src/Types/ContextFactory.php b/vendor/phpdocumentor/type-resolver/src/Types/ContextFactory.php
new file mode 100644
index 000000000..892ee0f90
--- /dev/null
+++ b/vendor/phpdocumentor/type-resolver/src/Types/ContextFactory.php
@@ -0,0 +1,420 @@
+<?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;
+ }
+}